本文へ移動

ホーム / Web / 第3部 フロントエンド

第3部 フロントエンド・第5章

JavaScript と TypeScript

★★★ 必須9トピック・面接 5問

フロントエンドとバックエンド(Node.js)の両方で使う言語の、動きの仕組みを押さえる。

ひとことで

JavaScript は1本のスレッドで動き、待ち時間のある処理はキューに入れて後から実行する。

ブラウザの JavaScript は、基本的に1本のスレッド(メインスレッド)で動きます。画面の描画やクリックの処理も同じスレッドなので、通信やタイマーの待ち時間にスレッドを止めないよう、「終わったら後で呼んでね」とキューに入れる仕組みになっています。この順番を回すのがイベントループです。

スタックが空になるたびに、この順で取り出すコールスタック空になったら次へcalc()onClick()(最初のコード)① マイクロタスク:空になるまで全部Promise.thenawait の続きqueueMicrotask② 必要なら描画(レイアウト・ペイント)③ タスク:1つだけsetTimeoutクリック通信の完了
イベントループが仕事を取り出す順番
しくみ入るもの
コールスタックいま実行中の関数。空になるまで他は動かない
マイクロタスクキューPromise の then・await の続き、queueMicrotask
タスクキューsetTimeout・クリックなどのイベント・通信の完了

順番は「スタックが空になる → マイクロタスクを全部実行 → 必要なら描画 → タスクを1つ実行 → またマイクロタスクを全部…」です。

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// 出力:1 4 3 2

面接ではこう答える

ブラウザの JavaScript は1本のメインスレッドで動き、描画やクリックの処理も同じスレッドで行います。待ち時間のある処理は、終わったら呼んでもらうようにキューに入れ、イベントループが順番に取り出して実行します。順番は、コールスタックが空になったらマイクロタスクを全部、必要なら描画、そしてタスクを1つ、の繰り返しです。だから Promise の then は setTimeout より先に動きます。

5.2Promise と async/await

用語★★★ 必須
ひとことで

Promise は「あとで決まる結果」。await はそこで一度手を離し、残りをマイクロタスクで続ける。関係のない待ちは Promise.all で並べる。

関係のない通信は、並べて待つ順番に await(A のあと B)300ms + 200ms = 500msPromise.all で同時に長いほうの 300ms
2つの通信を待つ時間の違い(例)

async 関数は Promise を返し、await の所で一度止まって、残りはマイクロタスクとして後で続きます。スレッドは止まらないので、待っている間に他の処理や描画が進みます。

// 順番に待つ(合計 = a + b の時間)
const a = await fetchA(); const b = await fetchB();
// 同時に待つ(合計 = 長いほうの時間)
const [a2, b2] = await Promise.all([fetchA(), fetchB()]);

互いに関係のない通信は Promise.all で並べます。1つの失敗で全体を失敗にしたくなければ Promise.allSettled。await を付け忘れたり catch しなかったりするとエラーが見えなくなるので、lint(no-floating-promises)で防ぎます。

関数結果使いどころ
Promise.all全部成功で配列。1つでも失敗したら、すぐ失敗全部そろわないと意味がない
Promise.allSettled全部の成功・失敗の一覧一部が失敗しても残りを表示したい
Promise.race最初に終わった1つ(失敗も含む)タイムアウトとの競争
Promise.any最初に成功した1つ複数の取得先のどれかでよい

キャンセルしたい通信(画面を離れた・検索語が変わった)は AbortController の signal を fetch に渡して止めます。

面接ではこう答える

Promise は、通信のように後で結果が決まる処理を表すオブジェクトです。async 関数の中で await すると、その場でスレッドを止めるのではなく一度手を離し、結果が出たら続きがマイクロタスクとして実行されます。互いに関係のない通信を順番に await すると合計の時間がかかるので、Promise.all で同時に待ちます。一部が失敗しても続けたいときは allSettled を使います。

5.3重い処理と Web Worker

用語★★☆ 重要
ひとことで

メインスレッドを 50ms 以上ふさぐ処理はクリックも描画も止める。分割して手を離すか、Web Worker に任せる。

長い処理を分けると、間にクリックへ応答できる分けない1つの処理 500ms(この間は応答できない)クリック分ける100ms100ms100ms100ms100msすぐ応答間で手を離す:setTimeout・scheduler.yield()/別スレッド:Web Worker
長い処理を分割すると、間にクリックへ応答できる

大きな JSON の加工や画像処理を 500ms 続けると、その間はクリックも描画も止まり、画面が固まって見えます(INP が悪くなる)。マイクロタスクを無限に積み続けても同じです。

  • 処理を小さく分けて、間に setTimeout や scheduler.yield()(対応ブラウザ)で手を離す。
  • Web Worker:別のスレッドで JS を動かす。DOM は触れないが、計算は任せられる。postMessage でデータをやりとりする。
  • そもそもサーバー側で処理する・量を減らす。

Node.js でも同じ仕組みで、重い同期処理があると、その間ほかのリクエストに応答できなくなります。

開発者ツールの Performance パネルで、50ms を超える処理(ロングタスク)は赤い印で表示されます。まずそこを見つけてから、分割・Worker・処理の削減を選びます。

面接ではこう答える

ブラウザの JavaScript はメインスレッドで描画やクリックの処理と同じ列に並ぶので、重い計算を続けると画面が固まります。50ms を超える処理はロングタスクと呼ばれ、INP が悪くなる主な原因です。対策は、処理を小さく分けて間に手を離すこと、計算を Web Worker の別スレッドに任せること、そもそもサーバー側に移すことです。

ひとことで

クロージャは、関数が作られた場所の変数を覚えて持ち歩く仕組み。this は呼ばれ方で決まり、アロー関数は外側の this を使う。

JavaScript の面接でよく聞かれる、変数の見える範囲と this の決まり方です。

用語意味気をつけること
スコープ変数が見える範囲。let・const はブロック({})ごとvar は関数単位で、宣言前に使えてしまう。今は使わない
クロージャ関数が、作られた場所の変数を覚えたまま持ち歩くことイベントの処理や useEffect の中で、古い値を持ち続けることがある
this普通の関数では「どう呼ばれたか」で決まるコールバックに渡すと変わる。アロー関数は外側の this をそのまま使う
function counter() {
  let n = 0;               // この n を覚え続ける
  return () => ++n;
}
const next = counter();
next(); next();          // 1, 2

React でよく出る「古い値を見てしまう」問題(stale closure)は、クロージャが作られたときの状態を覚えているために起こります。setCount(c => c + 1) のように、前の値を受け取る形で更新すると避けられます。

面接ではこう答える

JavaScript の関数は、作られたときの外側の変数を覚えたまま持ち歩きます。これがクロージャで、カウンターや設定を閉じ込めた関数を作るのに使います。逆に、React のイベント処理やタイマーの中で古い状態を見続ける問題の原因にもなります。this は普通の関数では呼ばれ方で決まりますが、アロー関数は外側の this をそのまま使うので、コールバックではアロー関数を使うことが多いです。

5.5TypeScript の型

用語★★☆ 重要
ひとことで

実行前に型の食い違いを見つける仕組み。any を避け、ユニオン型と型の絞り込みで安全にする。

TypeScript は JavaScript に型を足した言語で、実行前(コンパイル時)に「この値にこのプロパティはない」といった間違いを見つけます。エディタの補完や、名前の一括変更も安全になります。

型推論・ユニオン・ジェネリクス

// 型推論:書かなくても number と分かる
const count = 3;

// ユニオン:どれか1つ。kind で絞り込める
type Result =
  | { kind: "ok"; data: User }
  | { kind: "error"; message: string };
function show(r: Result) {
  if (r.kind === "ok") return r.data.name; // ここでは data がある
  return r.message;
}

// ジェネリクス:型を引数のように受け取る
function first<T>(items: T[]): T | undefined { return items[0]; }

「読み込み中・成功・失敗」のような状態は、ユニオンで表すと「成功なのに data がない」状態を作れなくなります。any を使うと型の確認が止まるので、分からない値は unknown にして絞り込んでから使います。tsconfig.json は "strict": true にします。

面接ではこう答える

TypeScript は JavaScript に型を足した言語で、実行前に、存在しないプロパティを使うといった間違いを見つけます。読み込み中・成功・失敗のような状態はユニオン型で表し、kind などで絞り込むと、成功なのにデータがない状態を作れなくなります。any を使うと確認が止まるので、分からない値は unknown にして絞り込んでから使い、tsconfig は strict にします。

ひとことで

フロントとバックで API の型を1か所から作って共有する。型はコンパイル時だけなので、外から来るデータは実行時にも検証する。

型はビルドで消える。外から来るデータは実行時にも確かめるAPI の仕様OpenAPI・スキーマ型を生成コンパイル時の確認実行時の検証Zod などで parse安全な値画面・処理で使う
型の共有と、実行時の検証の役割

API の型を共有する

フロントとバックで API の型が食い違うと、本番で初めてエラーになります。

方法仕組み向いている場面
OpenAPIAPI の仕様書から、TypeScript の型やクライアントを生成する(openapi-typescript など)バックエンドが別の言語、公開 API、App とも共有する
tRPCサーバーの関数の型をそのままフロントで使う。コード生成が要らないフロントもバックも TypeScript で、同じリポジトリ
GraphQLスキーマから型を生成する(GraphQL Code Generator)GraphQL の API を使っている

注意:型はコンパイル時だけのもので、実行時には消えます。API の応答やフォームの入力が本当にその形かは保証されないので、外から来るデータは Zod などで実行時にも検証します。

面接ではこう答える

フロントとバックで API の型がずれると、本番で初めてエラーになります。そこで OpenAPI の仕様から型を生成したり、両方が TypeScript なら tRPC でサーバーの型をそのまま使ったりして、1か所から型を作ります。ただし型はコンパイル時だけのもので、実行時には消えます。API の応答やフォームの入力は Zod などで実行時にも検証して、形が違えば早めにエラーにします。

ひとことで

ES Modules でファイルを分け、ビルドツールが TypeScript・JSX を変換し、配信用にまとめる。

今の JavaScript は import / export(ES Modules)でファイルを分けて書きます。ブラウザに届ける前に、ビルドツール(Vite・webpack・esbuild・Turbopack など)が、変換・まとめ・圧縮を行います。

書いたコードが、ブラウザに届く形になるまでソース.ts・.tsx・importトランスパイル型と JSX を JS にバンドルまとめる使わない export を除く圧縮main.3f9a.js最初に読むchart.8c1d.jsimport() で後からapp.51e2.css型の確認は別にtsc --noEmit
ビルドツールがソースを配る形に変える流れ

トランスパイル

TypeScript の型や JSX(<div> を JS の中に書く記法)はブラウザがそのまま読めないので、普通の JavaScript に変換します。これをトランスパイルと呼びます。esbuild や SWC は型の確認をせず変換だけ行って速いので、型の確認は tsc --noEmit を別に流します。古いブラウザ向けに新しい書き方を変換することもあります(対応範囲は browserslist で指定)。

面接ではこう答える

今の JavaScript は import と export の ES Modules でファイルを分けて書き、ブラウザに届ける前にビルドツールが変換やまとめ、圧縮を行います。TypeScript や JSX はそのままでは動かないので、普通の JavaScript にトランスパイルします。esbuild や SWC は型を確かめずに変換だけするので速く、型の確認は tsc を別に流します。

「使わないものを消す」と「後で読む」は別の手段ツリーシェイキング・ビルド時に不要な関数を消す・静的な import だから分かる・最初の JS が小さくなるコード分割・import() で別ファイルに・必要になったら読み込む・ページ・重い部品ごと
ツリーシェイキングとコード分割の違い
  • ツリーシェイキング:import されていない関数をバンドルから取り除く。ES Modules の静的な import だから分かる。import _ from "lodash" のように丸ごと読むライブラリや、読み込むだけで副作用があるファイルは取り除けないことがある。
  • コード分割:import()(動的 import)や、ページごとの分割で、最初に要らないコードを後から読み込む。React なら lazy()。モーダルやグラフなど重くて最初に出ないものに向いています。
const Chart = lazy(() => import("./Chart"));

どのライブラリが大きいかは、バンドルアナライザー(rollup-plugin-visualizer・@next/bundle-analyzer など)で図にして確かめます。大きな日付ライブラリや、アイコンを全部読み込んでいるのが、よくある原因です。

ひとことで

Node.js は1本のスレッドのイベントループで多くのリクエストを回す。重い同期処理があると全員が待たされる。

1本のスレッドで多くのリクエストを回す得意・通信・DB の待ちが多い処理・多くの接続を少ない資源で・フロントと同じ言語苦手・注意・重い計算(画像・暗号・大きな JSON)・同期の API(readFileSync)・処理されない Promise の失敗
Node.js の得意なことと注意すること

Node.js はブラウザと同じイベントループで動くので、待ち時間の多い Web の API には向いていますが、1つのリクエストが重い計算をすると、その間ほかの全員が待たされます。

注意対策
重い同期処理worker_threads に任せる・キューで別のワーカーに回す
*Sync の APIリクエストの処理の中では使わない(起動時だけ)
捕まえていない Promise の失敗既定でプロセスが終了する。必ず catch し、終了時は落ちる前提で再起動させる
大きなファイル・応答Stream で少しずつ流す。全部メモリに読まない
CPU を使い切れない1プロセスは1コアが中心。コンテナを複数動かすか、cluster で複数プロセスにする

イベントループの詰まり具合は、perf_hooks の monitorEventLoopDelay や APM の「イベントループの遅延」で測れます。

面接ではこう答える

Node.js は、通信や DB の待ちの間に他のリクエストを進めることで、少ない資源で多くの接続を扱えます。逆に、画像処理や大きな JSON の加工のような重い同期処理を1つのリクエストで行うと、その間ほかのリクエストが全部止まります。そういう処理は worker_threads やジョブのキューに回します。捕まえていない Promise の失敗でプロセスが落ちるので、エラー処理と再起動の仕組みも必須です。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • イベントループと非同期(Promise・async/await)を説明できる
  • TypeScript の型でバグを防ぐ考え方
  • イベントループと非同期:JavaScript は1本のスレッドで動き、待ち時間のある処理はキューに入れて後から実行する。
  • Promise と async/await:Promise は「あとで決まる結果」。await はそこで一度手を離し、残りをマイクロタスクで続ける。関係のない待ちは Promise.all で並べる。
  • 重い処理と Web Worker:メインスレッドを 50ms 以上ふさぐ処理はクリックも描画も止める。分割して手を離すか、Web Worker に任せる。
  • スコープ・クロージャ・this:クロージャは、関数が作られた場所の変数を覚えて持ち歩く仕組み。this は呼ばれ方で決まり、アロー関数は外側の this を使う。
  • TypeScript の型:実行前に型の食い違いを見つける仕組み。any を避け、ユニオン型と型の絞り込みで安全にする。
  • API の型の共有と実行時の検証:フロントとバックで API の型を1か所から作って共有する。型はコンパイル時だけなので、外から来るデータは実行時にも検証する。
  • モジュールとトランスパイル:ES Modules でファイルを分け、ビルドツールが TypeScript・JSX を変換し、配信用にまとめる。
  • Node.js をサーバーで使うときの注意:Node.js は1本のスレッドのイベントループで多くのリクエストを回す。重い同期処理があると全員が待たされる。

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1イベントループについて説明してください。

模範回答

JavaScript は1本のスレッドで動き、実行中の処理はコールスタックに積まれます。通信やタイマーのように待つものは、完了したらキューに入り、スタックが空になってから実行されます。これを回す仕組みがイベントループです。

キューには2種類あり、Promise の then や await の続きはマイクロタスク、setTimeout やクリックなどのイベントはタスクに入ります。スタックが空になると、まずマイクロタスクを全部実行し、必要なら描画し、そのあとタスクを1つ実行する、を繰り返します。なので setTimeout 0 より Promise.then が先に動きます。重い同期処理があるとこのループが止まり画面が固まるので、処理を分けるか Web Worker に移します。

深掘りされたら

  • マイクロタスクを積み続けるとどうなりますか?

見られているポイントマイクロタスクとタスクの順番まで

Q2TypeScript を使う利点と注意点は?

模範回答

利点は、実行する前に型の間違いを見つけられることと、エディタの補完や名前の一括変更が安全になることです。人が増えたり規模が大きくなったりしても、型が仕様書の代わりになって変更しやすくなります。ユニオン型で状態を表すと、ありえない状態を作れなくなるのも便利です。

注意点は、型はコンパイル時だけのもので実行時には消えることです。API の応答やユーザーの入力が型どおりとは限らないので、境界では Zod などで実行時の検証をします。また any や as で型を曲げると守りがなくなるので、strict を有効にして unknown から絞り込むようにしています。

深掘りされたら

  • API の型をフロントとバックで共有するにはどうしますか?

見られているポイント型の限界(実行時の検証は別)も話せるか

Q3Promise.all と Promise.allSettled の違いは? 使い分けは?

模範回答

Promise.all は、渡した処理がすべて成功したら結果の配列を返し、1つでも失敗するとすぐに失敗になります。Promise.allSettled は、全部が終わるまで待ち、それぞれの成功・失敗をまとめて返します。

例えば、画面の表示に全部のデータが必要で、1つ欠けたら意味がないなら Promise.all を使います。ダッシュボードの複数のウィジェットのように、1つ失敗しても他は表示したいなら allSettled を使って、失敗した所だけエラーを出します。どちらも並行して動くので、関係ない通信を await で1つずつ待つより速くなります。Promise.all で失敗しても、他の処理自体は止まらない点にも注意します。

深掘りされたら

  • await を付け忘れるとどうなりますか?

見られているポイント失敗のときの動きの違いと、使い分けの例

Q4画面が固まるという報告がありました。JavaScript の側で何を調べますか?

模範回答

開発者ツールの Performance パネルで操作を記録し、メインスレッドを長く占めているタスク(50ms を超える長いタスク)を探します。どの関数が時間を使っているかが分かるので、大きな配列の処理、重い JSON の変換、大量の DOM の更新や再描画などを特定します。

直し方は原因によって、処理の量を減らす、仮想化して見える行だけ描く、処理を小さく分けて間で手を離す、Web Worker に移す、などです。実ユーザーで起きているかは、RUM の INP で確かめます。直したら同じ操作で測り直して、長いタスクが消えたことを確認します。

深掘りされたら

  • Web Worker で DOM を触れないのはなぜですか?

見られているポイント長いタスクの見つけ方と対策

Q5ツリーシェイキングとコード分割の違いは何ですか?

模範回答

ツリーシェイキングは、import されていないコードをバンドルから取り除くことで、送る量そのものを減らします。コード分割は、送るコードを複数のファイルに分け、最初に要らないものを後から読み込むことで、最初の読み込みを軽くします。

例えば、グラフのライブラリはモーダルを開いたときだけ使うなら、import() や React の lazy で別のファイルにします。ツリーシェイキングは ES Modules の静的な import が前提なので、丸ごと読み込むライブラリや副作用のあるファイルでは効かないことがあります。どちらもバンドルアナライザーで、何が大きいかを見てから手を入れます。

深掘りされたら

  • バンドルが大きくなる原因でよくあるものは?

見られているポイント減らすのと分けるの違い