Open9

Ozonlabsの開発で行ったチューニングの話や詰まった事など

jpnykwjpnykw

現在、フロントエンド・バックエンドにNextJS、インフラにVercel、認証にFirebase Auth、データベースにPlanetScale(via Prisma)という構成で新規サービスを全て1人で開発しています(2022/12/16、無事にサービスがリリースされました!)

また、サービス本体だけではなくユーザーや記事を管理するためのツールも開発しました(使っている技術スタックはほとんど同じです)。そちらも含めての話になります。

読み込み速度やパフォーマンスチューニングを頑張った(頑張っていきたい)ので、やったことをメモがてらにまとめます。変な場所でコケたりもしたので覚えてることを共有しておきます。もし誰かの救いになると嬉しいです。一応Lighthouseのスコアはこんな感じになっています。

jpnykwjpnykw

Reactの話

メモ化

基本的な話ですが、ハンドラなどで即時関数を使いません。なぜなら、コンポーネントの再レンダリング時にコールバックが再生成されてしまうことを防ぐためです。

また、コンポーネント内部で定義する際にはuseCallbackを使ってメモ化してあげたほうが良いです。更新頻度が高いコンポーネントの場合、再レンダリング時に全てのコールバックを再生成します。基本的には使いまわして良いものなのでuseCallbackでラップしてあげましょう。

// ❌
const handleSomething = () => { ... }

// ✅
const handleSomething = useCallback(() => { ... }, [deps])

また、値についてもコールバック同様です。Hooksに依存した値を用いた計算などはその都度計算せずにuseMemo を使いましょう。特に計算コストがかかる処理は必須です。

// ❌
<div>{doSomething(value)}</div>

// ✅
const calculatedValue = useMemo(() => doSomething(value), [value])
<div>{calculatedValue}</div>

最後に、更新頻度が高いコンポーネントは React.memo でメモ化することです。propsが変更されたときに再レンダリングされます。

Concurrent Rendering

体験的な話ですが、ページ全体のレンダリングをキャンセルさせたくなかったので記事のレンダリング等にはSuspenseを使っています。

Lift State Up & Down

Reactはコンポーネント内部でstateをバリバリに扱うものだと思いますが、そのstateが一部に依存しているのであれば、それはコンポーネント化しています(Lift State Down)。

そうしないと更新時に関係のないコンポーネントも巻き込んでしまうためです。無駄なレンダリングを避けて実行時のパフォーマンスを上げるためです。同様なケースで、状態を共有したい場合はstateを親コンポーネント側に移動させます(Lift State Up)。

状態管理

最初は Recoil 辺りを使おうかと思ったのですが、規模感的にContext APIで十分そうだったので採用しませんでした。ただ管理ツールの方では若干データフローが煩雑になってきている気もするので導入しても良さそう。

jpnykwjpnykw

Nextの話

SSG/ISR/SSR/CSRの使い分け

コンテンツの更新頻度が低いので基本的にSSGで、トップページだけISRとCSRの合せ技をしています。1ページに描画できる範囲はISRしておいて(FHD基準で決めている)、それ以降の更新リクエストであったりクエリが更新された場合はSWR使って再度Fetchしています(CSR)

もう少し具体的には、例えばデータベースに50個の記事があったとして、また1画面に描画する記事を10個と決めておけば、最初の10個だけISRします。なので1ページ分の表示はすぐに行われます。そして、Ozonlabsは useSWRInfinite を使った無限スクロールによる読み込みを実装しているので、必要であれば都度追加データを取得します。その際の1ページ分の差分は計算して取得します。

また、記事についているタグをクリックすることで絞り込みが行なえます。その際には再度データを取得します。現在はコンテンツが少ないですが、今後配信が始まってコンテンツ量が増えてきたらこの辺りが効いてくると思っています。

TTFBを減らす

そもそもOGPを動的に生成するためにSSRをしていたのですが、思い切ってSSGに切り替えました。そうしたら5倍ほど速くなりました(画像参照)


とはいえ現状Vercelが足を引っ張っている感じが否めません。(SSGしても平均して900ms前後かかってしまう)なのでGCP等に移行することも検討中…

LRUキャッシュ

node-lru-cache を使ってAPIの呼び出しにRate Limitを実装しています。

robots.txt is not valid の対応

ISRを効かせている場所の robots.txt がおかしなことになっちゃったので、APIを用意してちゃんと対応してあげました。

// next.config.js

async rewrites() {
  return [
    {
      source: '/robots.txt',
      destination: '/api/robots'
    }
  ]
},
// pages/api/robots.ts

/** @format */
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(_: NextApiRequest, res: NextApiResponse) {
  res.send(`User-agent: *
Allow: /`)
}

セキュリティ

https://securityheaders.com/ の評価を基準にしてヘッダを設定しました。

nextではこういう感じで設定できます。

// next.config.js

async headers() {
  return [
    {
      key: 'X-DNS-Prefetch-Control',
      value: 'on'
    },
    // etc...
  ]
}
jpnykwjpnykw

Firebase Authの話

signInWithPopup は使わないほうが良い

PC等では問題なく動くので実機検証をするまで気が付かなかったこと。iOSではサイトを超えたトラッキングがデフォルトで無効化されているので、ログイン処理がうまく動きませんでした。代わりに signInWithRedirect を使って実装しました。

jpnykwjpnykw

iOSの話

Bearerスキームの罠

開発中にBasic認証をかけていたらログイン処理が動かなかった。iOSのSafariだとAuthorizationヘッダをBasic認証が上書きしてしまうらしいです。

CSS Animationが効かない

これは CriOS で発生した問題です(ChriOSはChrome iOSのUA)解決策としてそもそもtransitionで実装してしまうとかがあるらしい。現状はUAを見てアニメーションのオン/オフを切り替えています。これは後でtransitionに統一したい。

jpnykwjpnykw

FOUC対策

ページの読み込み時に一瞬だけCSSが剥がれてしまって起こるちらつきのことです。styled-componentsを用いていたのでほぼ これ を参考に実装しました。

jpnykwjpnykw

コーディングの意識的な話

今回の開発はすべて1人で行ったので、当然ですがコードレビューをしてくれるエンジニアはいません。なので、自分でPRを建ててそれを自分でレビューをするという謎の行為をしていたのですが、何も確認せずに本番に取り込まずに1度PRを立てて差分を再確認することは割と有用でした(その段階でミスやバグを見つけることが何度かあったので)

また、開発をするに当たって次のような目標を掲げて行っていました。

  • 0 any
  • 0 warning
  • 0 error

まず0 anyというのは当然型の話です。TypeScriptを採用しているので、当然ですが型の恩恵を受けたいです。当たり前かもしれませんがany型は使っていません(まあanyが必ずしも悪であるとは限らないですが)また、目的としてはタイプセーフよりもコーディング時に起こりうるヒューマンエラーを補完機能で防ぐ目的で型を使っています。当然タイプセーフも大事ですが。

ある程度先を見据える

Nextの話 の使い分けにも書いたような、今のうちに想定できることを考えた実装をしています。後からチューニングすることは当然発生しますが、やれるのであれば先にやっておいたほうが後の保守コストは下がります。

jpnykwjpnykw

セマンティクス・アクセシビリティ

サービスを利用してくれるユーザーは多種多様です。多くの人々が同じ様にコンテンツを利用できるようにするためにはアクセシビリティを意識する必要があります。またそこにはHTML5のセマンティクスを知ることと関係があります。

要点

  • 背景とのコントラスト差に気をつける
  • 要素に対して適切な属性を付与する(逆もしかり、不適切な属性を付与しない)
    これはキーボードユーザーなどへの配慮です。インタラクティブが可能な要素(クリックしたりなど)はdivなどを使わずbuttonを使うほうが良いとされています。
  • コンテンツを見出し(H1など)より前に配置しない

Ref: