おじんブログ

Webアプリに関する知見とか雑記です

ゆめみ社のフロントエンドコーディング試験をやってみた

取り組んだものは下記です。

notion.yumemi.co.jp

応募する予定はありませんが、フロントエンドの素振りの課題としては面白そうと思って取り組んでみました。

やってみた感想として、適切に作ろうと思うと意外に設計に頭を使うのと、Unit Test の経験値がないことを突きつけられて面白かったです。 自分にはフロントエンドの実務経験が一切ないので、自分の中でバラバラに知見があった ESLint や Prettier、husky や GitHub Actions をアプリケーション開発の中で使用する、経験が得られてよかったです。

また、コミットやブランチの運用も採点対象になるそうなので、普段ものすごい雑にやっていたコミットやブランチ運用に緊張感を持って、他人が見ることを意識して取り組めました。

テストは 100%カバレッジを通そうとしましたが、モチベーションが尽きてしまったのでこれで終了です。

------------------|---------|----------|---------|---------|-------------------
File              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files         |   97.22 |    97.43 |   95.23 |   97.22 |
 src              |     100 |      100 |     100 |     100 |
  setup.ts        |     100 |      100 |     100 |     100 |
 src/components   |     100 |      100 |     100 |     100 |
  Chart.tsx       |     100 |      100 |     100 |     100 |
  Prefectures.tsx |     100 |      100 |     100 |     100 |
  Title.tsx       |     100 |      100 |     100 |     100 |
 src/lib          |   95.02 |    95.83 |     100 |   95.02 |
  client.ts       |     100 |      100 |     100 |     100 |
  constants.ts    |     100 |      100 |     100 |     100 |
  hooks.ts        |   84.84 |     92.3 |     100 |   84.84 | 21-24,45-46,49-52
 src/test         |     100 |      100 |      80 |     100 |
  utils.ts        |     100 |      100 |      80 |     100 |
------------------|---------|----------|---------|---------|-------------------

成果物

着手して 10 日ぐらい経っているが、実動は 24 時間 ( 3 日 ) ぐらい。本業が忙しかったり体調を崩してなかなか進めなかったのと、 Vitest の設定にちょっと苦戦したので思ったより時間がかかってしまった。

population-app-iota.vercel.app

リポジトリ

github.com

注意した点

dependencies を最小にする

  • 脆弱性を極限まで減らすため。また、依存関係をシンプルにして React の知識があれば誰でも読み解けるようにするため
  • 基本的に Web API、React の API だけ使って構築する。Redux も使わない
  • グラフは自作するのが大変なので recharts を使った
  • 最終的に 3 つのライブラリだけ dependencies になった

github.com

Linter と Formatter の設定

  • プロジェクトを作ったら 2 コミット目で設定完了するぐらいの勢いで早めにやっておいた
  • huskylint-staged を使ってローカルで自動実行できるようにした
    • Unit Test と違ってこの辺りは実行環境に影響されないため
  • 手動実行を信じない、CI の Lint や Formatter で弾かれないようにした

Unit Test

  • テストと実装をセットで考えた
  • Jest に近い APIVitest を使ってみた
  • 基本的にテストしやすい形で書くので、関数の責務を小さくする必要があった
    • 引数や返り値を適切に設計する必要があった
  • React をテストするのに @testing-library/* 系のテストライブラリの使い方を理解した

CI/CD の設定

  • CI は GitHub Actions で設定
  • 適切にキャッシュを使って実行サイクルを速く下
  • 各ステップ毎に name を振ってどこまで終わったかを分かりやすくした
  • 実行環境に左右されないように、Unit Test は CI で実行し、ローカルでは実行しない方針にした
    • Node.js ではあまり影響はないと思うが、実行環境を統一することでトラブルを減らした
  • CD は Vercel で設定
    • お金をかけたくなかったのと、設定が楽なので採用した。Pull Request にプレビューホスティングされるのありがたい
  • API キーを漏らさないために、ローカルでは .env.local を、CI/CD では環境変数から API キーを渡すようにした
    • アプリケーションを Chrome の開発コンソールの Network タブを見てしまうとバレてしまうが仕方ない

反省点

  • モックを適切に使ってカバレッジ 100%にしたかったが、できなかった。もっと簡単な例で素振りをする
  • Custom Hooks が汎用性のない、ロジックの切り出しになったため、再利用性が薄くなってしまった
    • 重複したところも多い。また、適度な抽象化を行いたい
  • Prop Drilling になってしまったため、素直に React.createContext() でコンテクストを使えばよかった
  • テストの書き方も Example を見つつ書いているが、どこまで書けばいいかわからなかった
    • ここら辺は実務経験がないことがネックになっていると感じる
    • とはいえこれはグループ毎に文化が分かれていそう

まとめ

テックリード職を前提として取り組んだが、フロントエンドのテックリードなら 1 つの JavaScript フレームワークに精通して、Unit Test のテストフレームワークの使い方や書き方を覚えていて当然とは感じた。

複数の JavaScript フレームワークを知っていることより、Linter、Formatter、Unit Test を CI 上に乗せて一連の流れを構築できる方が、開発する側としては大事だと改めて感じた。

個人的に TypeScript に関しては「型があるから JavaScript」より難しいとは思わず、寧ろ実行する前にエラーになる原因が明らかになる点で JavaScript よりは難易度が低いと思っている。

当初の目論見通り、JavaScriptフレームワークの素振りとしてはよかった。

個人だと、どうしても動けばいいレベルで、あまり複雑にならないためテストがおざなりになってしまうので、いい勉強になった。

2022 年度に習得&復習したい Web 技術

誰向け?

  • 2022 年度内に Web フロントエンドポジションに転職したい自分の整理
    • エンジニアとして 4 年目だけど Web アプリの開発経験があんまりなくて詰みかけている
  • 友人
  • その他、Web フロントエンドのトレンドをざっくり追いたい人

それぞれの技術の詳細は書きませんが、ざっくり気になるポイントを書いてます。

情報源

どんなものが流行っているか、という肌感のチェックに使っている。

フレームワーク&ライブラリ

React 18

  • 17 は特に大きな変更なしだったが、18 では色々な Hooks が追加されりしている
  • New Feature の項目を見ると使いこなすとパフォーマンスが上がりそうなので試してみる
  • Zenn とかでも解説記事が出回っているので書き方を探っていく

Next.js

  • 何度か個人的に作っているが、React で SG や SSR するならこれ
  • 全体的に Zero config でよしなにやってやる、という圧が相変わらず強い
  • 最近 next/jest Plugin ができたので試してみたい
  • 開発元の Vercel が Svelte や SWC の作者を雇用していたりと OSS の影響が今後も高そう

Svelte

  • ひとまずチュートリアルをやってみたが、極力責任範囲が少ない React よりは実開発向けに機能が考えられている感じ
  • Animation がビルトインされているのが面白い。CSS Animation を吐き出すようにしているにしている模様。
  • ReduxReact.createContext っぽいグローバルステート管理もビルトインされている模様。
  • フレームワークの思想は面白いとはいえ癖がある上に採用事例もまだ少ないので日本で使う現場があるかというと微妙。

Prisma

  • Node.js 向け ORM だが、CLI の操作や Client がかなり使いやすい。TypeScript に優しい。
  • schema 定義ファイルに沿って DB を変更したり、現在の DB から schema 定義ファイルを生成することも可能らしい

ツール

Vite

  • フロントエンド開発ツールに No Bundle という風潮を呼び込んだ今一番熱いビルドツール
  • かなり軽量でサクサク動く。今後 SSR しない静的なフロントエンド作るならこれが第一候補になると思われる。
  • The State of JS でも注目度が高い模様。
  • 公式ドキュメントが日本語化されているのもありがたい。
  • Vite 本体だけでなく、Vite で実行するテストフレームワークVitest や Storybook の Vite 版みたいな Ladle も出始めているので熱そう。

esbuild

  • TypeScript で書いたスクリプトをサクッと実行するのに使っている。
  • フロントエンドの JS をビルドするのにも使えるらしいが、ちょっとまだできていない
  • 型チェックは tsc でやって実行はこれが主流になると思う
  • Vite 内部でも使われている。

Playwright

Lighthouse

  • ざっくり数字見ていい感じ、ぐらいにしか見てないのでちゃんと読める読めて改善するための指標にする
  • Lighthouse CI で CI に突っ込めるので試してみたい

インフラ&その他

GitHub Actions

  • CI/CD のパイプラインを初期段階でシュッと作って開発体験を上げてテスト書く習慣を体に馴染ませたい
  • 他人が書いたパイプラインを読めるようにもしたい
  • CI/CD を前提とした開発をしないとどんどん手作業で複雑化する、というのを体感している

Firebase v9

  • v9 で結構 API が変わって TreeShake しやすくなったらしい
  • 業務で使うかは微妙だが、Firebase Auth 試したい試していとずっと思っていたので今年中に試す

AWS Amplify, Lambda, S3, Cloudfront

  • インフラが苦手なので、少なくともフロントエンド関係のサービスは触れるようにしたい
  • Serverless Framework でデプロイ作業をコード化できるのでそれも試す
  • サーバーレスアーキテクチャーと常時稼働のサーバーでどんなメリットデメリットがあるのかは言えるようにしたい

まとめ

自分が Web フロントエンドや自動テストの実務経験がほとんどなく、WordPress でゴニョゴニョして手動テストしたのがまともな実務上の開発経験なので、個人勉強である程度モダンな開発の経験値溜めておきたい。

プログラミング2021年振り返り

プログラミング勉強

全体

自分はフロントエンド得意だと思っていたのですが、「標準のHTMLのフォームでデータ送信するときってどんな感じでやり取りするんだっけ?」「mimeTypeやHeadersって知っているけどどういう仕組みだっけ?」「Web Workerって?シングルスレッドって?」「PWAってどういうものだっけ?」と基礎的なことで詰まっていたため、改めてMDNのHTTPのドキュメントを読んでサンプル書いて勉強していました。

developer.mozilla.org

ブラウザで使えるAPIについても目について面白そうなJavaScriptAPICSSをCodesanboxで書いて試していました。 Codesandboxを使った理由として、WindowsiPhoneなどの別環境での挙動確認だったりHTTPSでしか動かないAPIを試すために毎回デプロイするのは面倒なので書いてすぐ公開、Webブラウザ上(=閲覧環境)で編集できるためです。

JSのライブラリを使わず素のJSで書いて解決できるとありがたい場面は業務でも時々あるのでこういう素振りは役に立ちました。

developer.mozilla.org

codesandbox.io

アウトプットのリポジトリ

業務では経験できないことをちゃんと学習しようと思ってフロントエンドを中心にサンプルをザクザク作っていました。

github.com

Vite と Redux の学習のために作ったリポジトリ。 このあたりからViteを使い始めたのですが、既存プロジェクト以外はViteで開発するのが主力になるんじゃないかと思う程ビルドが高速でConfigも楽で快適だと思いました。

github.com

去年Next.jsで作った映画を公開順で閲覧するためのサイトですが、GitHub Actions で CI を回せるようにしました。 CI/CD に入門するためにGitHub Actionsを使いましたが、本当に無料でいいの?というぐらい機能が豊富で簡単です。 公式ドキュメントを読んでもいいし、テンプレートを選んでそこからカスタムしてもOK。

github.com

Next.jsのStatic Generateを学習するためにSpace XのAPIを使って作ったサイトです。 外部リンクからの画像最適化ってどうすればいいんだっけ?と頭を悩ませましたがVercelにデプロイする前提で解決しました。 Next.jsはReactベースのフレームワークで、ドキュメントに従って書けばなんでもよしなにやってくれてかなり便利な反面、ECMAScriptの挙動やTypeScript、ReactのHooksの書き方など、基礎的な箇所は意識して勉強しないといいコードにはなりませんね。

仕事

現在の会社に転職して2年8ヶ月経過しました。 転職前は2年半在籍していたのが最長だったので、今まで一番長い期間在籍しています。

前々からTwitterで呟いていたのですが、転職を考えています。

理由としては自分がフロントエンドが比較的得意なのですがそれを活かせる場面がなく、テストコードやCIもなく開発体制もかなり古いというのがずっと続いているためです。 自分である程度社内を改善しなきゃダメだと思って社内研修をするなど頑張っていましたが、あんまり改善できなかったのと、上司から評価されなくて、疲れて諦め気味です。

30歳になったのですが、まともな業務での開発経験がRedmineWordPressプラグインVBAぐらいしかありません。 そのため、実務でフロントエンドができていないことがコンプレックスになりかけています。

そういう状況で趣味の学習を元手にフロントエンドの開発職を狙うのは絶望的かもしれませんが、頑張っていきたいです。

2022年やりたいこと

  • フロントエンドのトレンドのキャッチアップ
  • Rustの学習入門、ライブラリ作成
  • npmのライブラリの公開パイプラインの構築
  • BFFの構築とGraphQL入門
  • AWSGCPなどのインフラ構築
  • テストコードいっぱい書く

Rustに関してはJavaScriptのツールチェインがRustベースで書かれていることが最近増えてきたのと、TypeScriptは便利だけどそれ以外の型付きの言語を学習してみたいのと、WASMのパフォーマンスを調べてみたいためです。

npmに関してはnpm publishコマンドで公開してみたのですが、ヒューマンエラーが起きそうで怖いのでGitHub Actionsでmainブランチにマージされたらpublishできるパイプラインを構築して本番に近い形で公開できる方法を練習したいです。 ただ個人で開発しているライブラリならnpm publishで公開になりそうです。

テストコードですが個人プロジェクトだと書く気がどうしても起きないのですが、業務で保守性の高いコードを書くには必要なので、個人勉強の範囲で練習して慣れておきたいです。

まとめ

フロントエンドは移り変わりが激しいと言われていたのは昔で大体ReactかVueかその派生フレーワークを使うことがトレンドで固まっていて、今後もその流れは強くなると思います。 ただReact 18の新機能やSvelteの登場、Web版PhotoshopでWeb ComponentsのライブラリであるLitが使われているそうなので、最先端の部分では動きがあるんじゃないかと思っています。

web.dev

ただ自分はまだ業務では素のJSかjQueryしか書いたことがないので、Reactを使う現場に転職できるように頑張ります。

もしこれを読んでいるをReactを使ったフロントエンドエンジニアを募集しているところがあったらお声掛けしていただけるとありがたいです。

良いお年を!

Inputタグにアップされた画像ファイルのプレビューを オブジェクト URLと データ URL でやる方法で書き比べてみた

動機

アップロードした画像を使って画像を加工するなどの処理の場合、どのように取り出して取り扱うか、というのが全然整理できていなかったのでその整理を込めてサンプルコードを書いた。

File オブジェクトや addEventListener もあんまり書いたことがなかったので、ライブラリなしの Vanilla JS で書いてみた。

やりたいこと

  • input[type="file"] に入力された画像はファイル名しか表示されないので、画像がプレビューされてほしい
  • fileを取り出した後に <img> タグに表示するためにURLに変換する必要があるが、アプローチとして URL.createObjectURL() を使う、 FileReaderreadAsDataURL() を使うアプローチがあった

TL;DR

Codesandbox で試せる環境を作った。勉強も兼ねてVanilla + TypeScriptで型をつけながら書いています。

  • 動作的にはどっちでプレビューしても殆ど変わりはない
  • URL.createObjectURL() してオブジェクトURLを生成すると、開放しない限りはメモリ上に残り続ける
  • なのでオブジェクトURLが不要になったタイミング、今回の場合だと画像の読み込みが終わったタイミングで URL.revokeObjectURL() を実行する必要がある
  • 開放するとそのオブジェクトURLを使って画像を表示、というのができなくなるので、複数箇所で同じ画像を表示するなら URL.revokeObjectURL() を実行するタイミングを考える必要がある
  • データURLはURL自体にデータを持っているようなものなのでオブジェクトURLよりは比較的取り回しが楽に感じた

解説

入力したファイルの取り出し

const fileForm = document.getElementById("userFile");

function handleFiles(event: Event) {
  const target = event.target as HTMLInputElement;
  const files = target.files;
  // ...
}

fileForm?.addEventListener("change", handleFiles, false);

input[type="file"] に対して document.getElementById("userFile").files という形で取り出せますが、フォームへの入力が変化したタイミングで処理を行いたいので EventListener 経由でアクセスして取り出しています。

取り出したファイルデータは複数の File データを持つ FileList オブジェクトとして取り出されます。

フォームから受け取りデータが単一の File オブジェクトではなく FileListオブジェクトなのは、フォームにて複数のファイルが入力する場合があるためです。

file からオブジェクトURLの変換

Array.from(files).forEach((file) => {
  // if you would like to watch uploaded file info, remove comment
  // console.log(file)
  const image = new Image(300);
  image.addEventListener("load", (event: Event) => {
    const target = event.target as HTMLImageElement;
    URL.revokeObjectURL(target.src);
  });
  const imageUrl = URL.createObjectURL(file);
  image.src = imageUrl;
  image.title = file.name;
  previewPoint?.appendChild(image);
});

まず new Image() にてプレビュー画像を表示するための <img> タグを生成しています。width=300 としたいのでコンストラクターに引数として渡しています。

オブジェクトURLは不要になったら開放する必要があるため、 <img> タグにて画像の読み込みが終わった時点で URL.revokeObjectURL() するように EventListener で登録します。

取得したFileを URL.createObjectURL() を通してオブジェクトURLを発行し、それを <img> タグの src に入力し、画像の表示を行います。

最後にプレビューするポイントにプレビュー画像のタグを付与する。

file からデータURLの変換

Array.from(files).forEach((file) => {
  const reader = new FileReader();
  reader.addEventListener(
    "load",
    (event: Event) => {
      const image = new Image(300);
      image.title = file.name;
      image.src = reader.result as string;
      previewPoint2?.appendChild(image);
    },
    false
  );
  reader.readAsDataURL(file);
});

FileReader.readAsDataURL() でファイルデータを読み込むとデータURLとして変換され、 FileReader.result にて取り出すことが出来る。

FileReaderがFileを読み込んだタイミングで FileReader.result にデータURLが入るため EventListener にて Event を登録する。

画像のタグを付与するあたりはオブジェクトURLと一緒。

実はこの箇所はMDNのサンプルコードのままではある。

最後に

フォームで入力した画像を取り出して取り扱う方法を学習することができた。

動機としては React Image Crop というライブラリを使おうと思った際に、当然のようにオブジェクトURLとbase64 data が登場したが、曖昧な理解だと今後も詰まりそうだと思ったのでWeb APIの標準を通じて学習した。

https://github.com/DominicTobias/react-image-crop

上記のオブジェクトURLを使ったサンプルコードを書いた後に、MDNのサンプルに自分がやりたいのとほぼ同じ物があったのでぐぬぬ…となったが、TypeScriptで型付けるという形で発展させた。

Web アプリケーションからのファイルの使用

Next.js で SG したサイトがなぜあんなに早いのか、 Chrome DevTools とコードリーディングで調べる

はじめに

静的コンテンツをCDNに乗せておくとアクセス時間が早い、という話ではなく、 Next.js を SG してブラウザで閲覧した時の挙動に絞っています。

TL;DR

  • SGした際に、 getStaticProps() 経由で渡したコンテンツは _next/data/ ディレクトリ配下に JSON 形式として書き出される
  • 該当するローティングに達すると、 JSON を読み込み、クライアントサイドでページを書き換える
  • すべてのコンテンツを一気に読み込むのではなく、ページへのリンクが表示されたら、そのリンク先のコンテンツの JSON を先読みしている
  • つまり、クリックする前に既にコンテンツは読み込まれているので、クライアントサイドの書き換えしか時間がかからない

Chrome DevTools で見てみる

Next.js の公式サイトは SG で構築されています。これを例として見ていきます。 もし初めて見て場合は各リンクをクリックしてみてください。 どのページも一瞬で遷移するのを感じると思います。

https://nextjs.org/

Chrome の DevTools を開き、 Network タブを開き、フィルター欄に json を入力し、一旦更新します。 するとJSONファイルが読み込まれているのがわかります。 f:id:mr_ozin:20211108224946p:plain

Headers タブを開いて Request URL 見ると、サイトのルートの /_next/data/ ディレクトリ配下にこのJSONファイルが格納されているのがわかります。 f:id:mr_ozin:20211108225015p:plain

Preview タブに切り替えてJSONの中身を見るとページの情報が格納されています。 f:id:mr_ozin:20211108225045p:plain

実際に該当のページに遷移すると、 JSON に格納したデータと一致するのを確認できます。

つまりページ遷移 = 読み込んだ JSON を JS でページの内容を書き換えるクライアントサイドレンダリングをしていると推測できます。

コードベースで言えば下記でビルド時にファイルパスを指定したマニフェストを作成し、ページロード時のコンテンツのパスを指定していると思われます。

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/build/index.ts#L1725-L1734

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/client/page-loader.ts#L141-L143

自作のサイトでも確認してみる

JSON の中の pageProps の構造について確証が持てなかったので自作のサイトでも確認してみることにしました。

https://nextjs-demo-spacex-crew.vercel.app/

自分のソースコードと比較すると、 pageProps 中の構造は Next.js の getStaticProps() で渡している構造と一致します。

つまり、ビルド時に渡したコンテンツのデータはJSONとして保存されていることが推測できます。 f:id:mr_ozin:20211108225109p:plain

https://github.com/ryokryok/nextjs-demo-spacex-crew/blob/b8812902315d0b111040f4980b1a70906faddcd0/pages/crews/%5Bid%5D.tsx#L30-L37

基本的にページコンテンツ必要になったら読み込む

ではブログの一覧画面のようなページが有り、リンク先が大量にあるようなケースでは読み込まれるJSONファイルの数が増えて、初期表示が遅れるのでは?と思いますが、その心配はありません。

Network タブを開いたまま、ページを上下にスクロールすると読み込まれる JSON ファイルが増えているのを確認できます。

ゆっくりスクロールするとわかりますが、ページのリンク が表示されたらその 遷移先のコンテンツのJSON が読み込まれます。

ユーザーが遷移先のリンクを閲覧する必要性が出てくるタイミングとして 「リンクが表示された段階」 を想定しており、そこで初めてJSONを読み込むように設計していることが推測されます。

リンクが表示された段階で既にコンテンツは読み込まれており、リンクをクリックするとクライアントサイドで書き換え、ページ内の画像やアセットの読み込みが実行されます。

これによって一気に読み込まれるJSONファイルの数を減らし、低速回線でも快適に閲覧できます。

コードベースで言えば next/link のコードを見ると Intersection Observer API を使って監視をしているのが確認できます。

(useIntersection という Hooks 経由で使用しています)

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/link.tsx#L249

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/use-intersection.tsx#L18

※ちょっと脱線しますが、 next/imageuseIntersection を使って監視しています。

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/image.tsx#L438

最後に

Next.js は基本的にマニュアルどおりに作成すれば適宜最適化され、表示速度の早い Web サイトを簡単に作成できます。

素の軽い HTML ページに比べて、初回の表示は微妙に劣ることはありえますが、2ページ目以降は先読みした JSON を元にクライアントサイドレンダリングするのが基本のため、ページ切り替えが早いです。

コードリーディングした箇所についてはあまり自身がないので、間違っていたら指摘お願います。

Next.js で SG したサイトがなぜあんなに早いのか、 Chrome DevTools とコードリーディングで調べる

はじめに

静的コンテンツをCDNに乗せておくとアクセス時間が早い、という話ではなく、 Next.js を SG してブラウザで閲覧した時の挙動に絞っています。

TL;DR

  • SGした際に、 getStaticProps() 経由で渡したコンテンツは _next/data/ ディレクトリ配下に JSON 形式として書き出される
  • 該当するローティングに達すると、 JSON を読み込み、クライアントサイドでページを書き換える
  • すべてのコンテンツを一気に読み込むのではなく、ページへのリンクが表示されたら、そのリンク先のコンテンツの JSON を先読みしている
  • つまり、クリックする前に既にコンテンツは読み込まれているので、クライアントサイドの書き換えしか時間がかからない

Chrome DevTools で見てみる

Next.js の公式サイトは SG で構築されています。これを例として見ていきます。 もし初めて見て場合は各リンクをクリックしてみてください。 どのページも一瞬で遷移するのを感じると思います。

https://nextjs.org/

Chrome の DevTools を開き、 Network タブを開き、フィルター欄に json を入力し、一旦更新します。 するとJSONファイルが読み込まれているのがわかります。 f:id:mr_ozin:20211108224946p:plain

Headers タブを開いて Request URL 見ると、サイトのルートの /_next/data/ ディレクトリ配下にこのJSONファイルが格納されているのがわかります。 f:id:mr_ozin:20211108225015p:plain

Preview タブに切り替えてJSONの中身を見るとページの情報が格納されています。 f:id:mr_ozin:20211108225045p:plain

実際に該当のページに遷移すると、 JSON に格納したデータと一致するのを確認できます。

つまりページ遷移 = 読み込んだ JSON を JS でページの内容を書き換えるクライアントサイドレンダリングをしていると推測できます。

コードベースで言えば下記でビルド時にファイルパスを指定したマニフェストを作成し、ページロード時のコンテンツのパスを指定していると思われます。

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/build/index.ts#L1725-L1734

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/client/page-loader.ts#L141-L143

自作のサイトでも確認してみる

JSON の中の pageProps の構造について確証が持てなかったので自作のサイトでも確認してみることにしました。

https://nextjs-demo-spacex-crew.vercel.app/

自分のソースコードと比較すると、 pageProps 中の構造は Next.js の getStaticProps() で渡している構造と一致します。

つまり、ビルド時に渡したコンテンツのデータはJSONとして保存されていることが推測できます。 f:id:mr_ozin:20211108225109p:plain

https://github.com/ryokryok/nextjs-demo-spacex-crew/blob/b8812902315d0b111040f4980b1a70906faddcd0/pages/crews/%5Bid%5D.tsx#L30-L37

基本的にページコンテンツ必要になったら読み込む

ではブログの一覧画面のようなページが有り、リンク先が大量にあるようなケースでは読み込まれるJSONファイルの数が増えて、初期表示が遅れるのでは?と思いますが、その心配はありません。

Network タブを開いたまま、ページを上下にスクロールすると読み込まれる JSON ファイルが増えているのを確認できます。

ゆっくりスクロールするとわかりますが、ページのリンク が表示されたらその 遷移先のコンテンツのJSON が読み込まれます。

ユーザーが遷移先のリンクを閲覧する必要性が出てくるタイミングとして 「リンクが表示された段階」 を想定しており、そこで初めてJSONを読み込むように設計していることが推測されます。

リンクが表示された段階で既にコンテンツは読み込まれており、リンクをクリックするとクライアントサイドで書き換え、ページ内の画像やアセットの読み込みが実行されます。

これによって一気に読み込まれるJSONファイルの数を減らし、低速回線でも快適に閲覧できます。

コードベースで言えば next/link のコードを見ると Intersection Observer API を使って監視をしているのが確認できます。

(useIntersection という Hooks 経由で使用しています)

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/link.tsx#L249

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/use-intersection.tsx#L18

※ちょっと脱線しますが、 next/imageuseIntersection を使って監視しています。

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/image.tsx#L438

最後に

Next.js は基本的にマニュアルどおりに作成すれば適宜最適化され、表示速度の早い Web サイトを簡単に作成できます。

素の軽い HTML ページに比べて、初回の表示は微妙に劣ることはありえますが、2ページ目以降は先読みした JSON を元にクライアントサイドレンダリングするのが基本のため、ページ切り替えが早いです。

コードリーディングした箇所についてはあまり自身がないので、間違っていたら指摘お願います。

Next.js と外部 API で作成した静的サイトをデプロイしようとしたら、画像最適化を考えて結局 Vercel にデプロイした

TL;DR

  • ローカルのリポジトリに入っていない、外部の API から取得した画像の最適化をどうするか
  • 結局、 Vercel なら外部 URL の画像も最適化して高速で配信できるので Vercel を選択した

経由

Next.jsの getStaticPropsgetStaticPaths を使ったStatic Generationの素振りをするために作成しました。

悩んだ末に結局 Vercel デプロイ

最初は Vercel にロックインはしたくないと思い、 next export で静的に書き出して firebaseNetlify にアップしようとしたしたのですが、 next export を使用する時は next/image が基本的に使えません。

なので、<img> タグで置き換えようかと思ったのですが、結局下記の理由で Vercel にロックインしてもいいので Vercel にデプロイすることにしました。

  • 今回のように外部 API を利用する場合はビルド時に画像を最適化するのは難しい
  • せっかく Next.js で高速な静的サイトを作成するのに、画像の表示がネックになるのは意味がないのでは?
  • Vercel のドキュメントを読んだら、外部 URL の画像も最適化して配信されることが判明
  • Vercel にデプロイするなら、 next export しないので next/image がそのまま使えて楽

Next.js は Vercel が作っているから親和性がいいのだろうと思っていましたが、真面目に考えると悩ましい画像の最適化・配信も担ってくれるので本当にありがたいです。

外部の API に含まれている画像 URL を next/image で使用する時の注意点

  • 表示する URL のドメインを調べて next.config.js に追記しておく必要があります。
  • もし画像のアップロード先のドメインが複数ある場合はそれらを列挙する必要があります。
  • もしドメインがない場合はエラーが出るので next.config.js に追記して dev server を再起動してください。

公式ドキュメント

https://nextjs.org/docs/api-reference/next/image#domains

今回、自分の場合は下記のように書きました。

/** @type {import('next').NextConfig} */
module.exports = {
  reactStrictMode: true,
  images: {
    domains: ['imgur.com', 'i.imgur.com', 'images2.imgbox.com'],
  },
}