Tips & Tricks (更新: 2026/6/7)

ブラウザが固まる重い処理、Web Workerで逃がす(Claude Code実装)

CSV集計や画像処理でUIがカクつく原因と対策。Claude CodeにWeb Workerを型付きで実装させ、メインスレッドを止めない設計を実例コードで解説。

ブラウザが固まる重い処理、Web Workerで逃がす(Claude Code実装)

数万行のCSVをブラウザで集計する画面を作っていたときのことです。「集計」ボタンを押すと、結果は出る。でも処理中の2〜3秒、スクロールが固まり、入力欄に文字が打てず、ローディングのくるくるすら止まりました。

カクついている、ではなく、完全にフリーズです。

原因はすぐ分かりました。重い計算を、画面を描く担当と同じ場所でやらせていたんです。料理人が一人しかいない店で、その人に大鍋をかき混ぜさせている間、注文も会計も全部ストップする。あれと同じでした。逃がし先が、Web Workerです。

この記事の要点

  • Web Workerは重いJS処理を別スレッドへ逃がす機能。これで計算中もUIが固まらない。Workerは画面(DOM)には触れず、データを受け取って計算して返す係に徹する。
  • Claude Codeに丸投げすると、Workerの中でdocumentを触る・通信がanyだらけ、になりがち。「何を作らないか」を先に渡すと事故が激減する。
  • CSV集計・画像処理・検索インデックス・ログ解析・JSON変換が典型的な使い所。逆に軽い処理にWorkerを足すと、起動コストで遅くなる。
  • 画像など大きいバイナリはtransferableで「コピーせず移動」させる。移動後の元データは使えなくなる点に注意。
  • ReactではuseEffectのcleanupで必ずterminate()。画面を離れてもWorkerが残る「終了漏れ」がメモリを食う最大の原因。

そもそもWeb Workerって何を解決するの

JavaScriptは基本的に「一本道」で動きます。画面の描画も、クリックの処理も、あなたの書いた重い計算も、ぜんぶ同じ一本の道(メインスレッド)を順番に通る。だから重い計算が道を塞ぐと、その後ろに並んでいる「スクロール」「文字入力」「アニメーション」が全部待たされる。これが冒頭のフリーズの正体です。

Web Workerは、この道をもう一本増やす仕組みです。重い計算を別の道(別スレッド)に移せば、メインの道は空いたまま。だから計算中でも画面はぬるぬる動きます。

ただし、別の道に移したWorkerには制約があります。画面(DOM)を直接いじれません。 ボタンの色を変えたり、文字を表示したりはできない。Workerにできるのは「データを受け取る→計算する→結果を返す」だけ。表示はメイン側の仕事、という線引きです。

この線引きが地味に大事で、後で「Claude Codeが勝手にWorkerの中で画面を触ろうとする」事故の予防線になります。最初に頭に入れておいてください。

どんな処理をWorkerに逃がすべきか

僕の基準はシンプルで、「押したらUIが一瞬でも固まる処理」だけ逃がします。具体的にはこのあたりです。

ユースケースWorkerに渡すもの返すもの
CSV集計CSV文字列行数、列名、数値列の平均や最大値
画像処理ImageDataのバッファグレースケール化したバッファ
検索インデックスドキュメント配列トークン化された軽量インデックス
ログ解析ログ文字列エラー数、警告数、上位メッセージ
重いJSON変換ネストしたJSONフラットなキーと値

逆に、数十件の並び替えや、軽いフィルタ、1回だけの小さな変換にWorkerを使うのは逆効果です。Workerは起動にも、データの受け渡しにもコストがかかる。軽い処理だと、そのコストのほうが計算より重くなって、かえって遅くなります。

僕は一度、念のためと思って数十件の配列のソートまでWorkerに投げて、計測したら素直にメインで回したほうが速かった、という間抜けをやりました。「重いから逃がす」のであって、「なんでも逃がす」ではない。ここを取り違えると意味がありません。

Claude Codeに指示するときのコツ

ここからが本題です。Web Workerは、Claude Codeに丸投げすると一番事故りやすいテーマのひとつだと思っています。理由は、Workerには「windowdocumentが使えない」という制約があるのに、AIは平気でそれを無視したもっともらしいコードを書くからです。型のないメッセージで通信を始めて、後から崩壊するパターンも多い。

なので僕は、最初のプロンプトで「成果物」「禁止事項」「検証方法」の3つを必ず書きます。特に禁止事項を先に渡すのがコツです。

Vite + React + TypeScriptの既存画面にWeb Workerを追加してください。

対象:
- src/workers/worker-protocol.ts
- src/workers/data.worker.ts
- src/hooks/useDataWorker.ts
- src/components/WorkerDemo.tsx

要件:
- WorkerはCSV集計、画像グレースケール、検索インデックス、ログ解析、JSON flattenを扱う
- メッセージはTypeScriptのunion typeで定義する
- React側はhookでWorkerを生成し、unmount時にterminateする
- ImageDataのArrayBufferはtransferableとして渡す
- Worker内ではDOM、window、document、React stateに触れない
- Playwrightまたは手動検査の手順を追加する

確認:
- npm run typecheck
- npm run test
- npm run devでUIが固まらないこと

「作って」だけだと、AIは自分の判断で楽な実装に寄せます。「Worker内ではDOMに触れない」「通信はunion typeで」と先に縛っておくと、出てくるコードの質が一段変わります。指示の粒度をそろえる考え方はCLAUDE.mdの書き方ガイドに書いたので、プロジェクト全体でAIの振る舞いを安定させたい人はそちらも。

まず通信の「契約」を1ファイルに集める

実装の順番も大事です。Worker本体から書き始めると、たいてい通信の型がぐちゃぐちゃになります。僕はいつも、メインとWorkerの「契約」を1ファイルにまとめるところから始めます。payload: anyで始めると、処理を1つ足すたびに型が崩れて、半年後の自分が泣きます。

// src/workers/worker-protocol.ts
export type CsvSummary = {
  rows: number;
  columns: string[];
  numeric: Record<string, { count: number; average: number; max: number }>;
};

export type ImageResult = {
  width: number;
  height: number;
  buffer: ArrayBuffer;
};

export type SearchDocument = {
  id: string;
  title: string;
  body: string;
};

export type SearchIndex = {
  documents: number;
  tokens: Record<string, string[]>;
};

export type LogSummary = {
  lines: number;
  errors: number;
  warnings: number;
  topMessages: string[];
};

export type JsonFlatResult = Record<string, string | number | boolean | null>;

export type WorkerJob =
  | { type: "csv:summary"; text: string; delimiter?: "," | "\t" }
  | { type: "image:grayscale"; width: number; height: number; buffer: ArrayBuffer }
  | { type: "search:index"; documents: SearchDocument[] }
  | { type: "log:summary"; text: string }
  | { type: "json:flatten"; value: unknown };

export type WorkerResultMap = {
  "csv:summary": CsvSummary;
  "image:grayscale": ImageResult;
  "search:index": SearchIndex;
  "log:summary": LogSummary;
  "json:flatten": JsonFlatResult;
};

export type WorkerRequest = {
  id: string;
  job: WorkerJob;
};

export type WorkerResponse<T = unknown> =
  | { id: string; ok: true; result: T }
  | { id: string; ok: false; error: string };

このファイルが、Claude Codeにレビューさせるときの基準にもなります。「新しい処理を足すときはWorkerJobWorkerResultMapを両方更新する」とルール化しておけば、通信仕様があちこちに散らばらない。初心者ほど、Worker本体よりこの契約ファイルを丁寧に作ったほうがいい、と僕は思っています。

Worker本体:計算だけに集中させる

次がWorker本体です。今回はCSV・画像・検索・ログ・JSONを1つのWorkerで処理しています。実務で大きくなったら分ければいいですが、最初は「UIから切り離す」ことだけを優先します。

// src/workers/data.worker.ts
import type {
  CsvSummary,
  ImageResult,
  JsonFlatResult,
  LogSummary,
  SearchIndex,
  WorkerRequest,
  WorkerResponse,
} from "./worker-protocol";

const workerScope = self as unknown as DedicatedWorkerGlobalScope;

workerScope.onmessage = (event: MessageEvent<WorkerRequest>) => {
  const { id, job } = event.data;

  try {
    const result = runJob(job);
    const response: WorkerResponse = { id, ok: true, result };
    // 画像のときだけバッファを「移動」させる(コピーしない)
    const transfer = resultHasBuffer(result) ? [result.buffer] : [];
    workerScope.postMessage(response, transfer);
  } catch (error) {
    const message = error instanceof Error ? error.message : "Worker failed";
    workerScope.postMessage({ id, ok: false, error: message } satisfies WorkerResponse);
  }
};

function runJob(job: WorkerRequest["job"]) {
  switch (job.type) {
    case "csv:summary":
      return summarizeCsv(job.text, job.delimiter ?? ",");
    case "image:grayscale":
      return grayscale(job.width, job.height, job.buffer);
    case "search:index":
      return buildSearchIndex(job.documents);
    case "log:summary":
      return summarizeLogs(job.text);
    case "json:flatten":
      return flattenJson(job.value);
    default:
      return assertNever(job);
  }
}

function summarizeCsv(text: string, delimiter: "," | "\t"): CsvSummary {
  const rows = text.trim().split(/\r?\n/).filter(Boolean);
  const headers = rows.shift()?.split(delimiter).map((cell) => cell.trim()) ?? [];
  const numeric: CsvSummary["numeric"] = {};

  for (const row of rows) {
    row.split(delimiter).forEach((cell, index) => {
      const key = headers[index] ?? `column_${index + 1}`;
      const value = Number(cell);
      if (!Number.isFinite(value)) return;

      const current = numeric[key] ?? { count: 0, average: 0, max: Number.NEGATIVE_INFINITY };
      current.count += 1;
      // 平均を逐次更新(全件ためずに済む)
      current.average += (value - current.average) / current.count;
      current.max = Math.max(current.max, value);
      numeric[key] = current;
    });
  }

  return { rows: rows.length, columns: headers, numeric };
}

function grayscale(width: number, height: number, buffer: ArrayBuffer): ImageResult {
  const pixels = new Uint8ClampedArray(buffer);

  for (let index = 0; index < pixels.length; index += 4) {
    const gray = Math.round(pixels[index] * 0.299 + pixels[index + 1] * 0.587 + pixels[index + 2] * 0.114);
    pixels[index] = gray;
    pixels[index + 1] = gray;
    pixels[index + 2] = gray;
  }

  return { width, height, buffer: pixels.buffer };
}

function buildSearchIndex(documents: Array<{ id: string; title: string; body: string }>): SearchIndex {
  const tokens: SearchIndex["tokens"] = {};

  for (const document of documents) {
    const words = `${document.title} ${document.body}`
      .toLowerCase()
      .split(/[^a-z0-9]+/g)
      .filter((word) => word.length >= 3);

    for (const word of new Set(words)) {
      tokens[word] = [...(tokens[word] ?? []), document.id];
    }
  }

  return { documents: documents.length, tokens };
}

function summarizeLogs(text: string): LogSummary {
  const lines = text.split(/\r?\n/).filter(Boolean);
  const counts = new Map<string, number>();
  let errors = 0;
  let warnings = 0;

  for (const line of lines) {
    if (/\berror\b/i.test(line)) errors += 1;
    if (/\bwarn(ing)?\b/i.test(line)) warnings += 1;
    const normalized = line.replace(/\d{4}-\d{2}-\d{2}[^\s]*/g, "").replace(/\s+/g, " ").trim();
    counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
  }

  const topMessages = [...counts.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5)
    .map(([message, count]) => `${count}x ${message}`);

  return { lines: lines.length, errors, warnings, topMessages };
}

function flattenJson(value: unknown, prefix = ""): JsonFlatResult {
  if (value === null || typeof value !== "object") {
    return { [prefix || "value"]: value as string | number | boolean | null };
  }

  return Object.entries(value as Record<string, unknown>).reduce<JsonFlatResult>((acc, [key, child]) => {
    const path = prefix ? `${prefix}.${key}` : key;
    return { ...acc, ...flattenJson(child, path) };
  }, {});
}

function resultHasBuffer(result: unknown): result is ImageResult {
  return typeof result === "object" && result !== null && "buffer" in result && result.buffer instanceof ArrayBuffer;
}

function assertNever(value: never): never {
  throw new Error(`Unsupported worker job: ${JSON.stringify(value)}`);
}

注目してほしいのは、画像処理だけArrayBufferをtransferableとして返している点です。transferableは「コピー」ではなく「所有権の移動」に近い。大きな画像や巨大なバイナリをコピーすると、それだけでUIが重くなります。移動なら一瞬です。ただし移動した元のバッファはもう使えなくなるので、メイン側で再利用しようとしない設計にしておきます。ここは後の「落とし穴」でもう一度触れます。

React hookで生成とcleanupを閉じ込める

React側では、Workerの生成・待機中のPromise・エラー・終了処理を、全部hookに閉じ込めます。コンポーネントの中で直接new Worker()を書くと、画面を移動したあともWorkerが残る「終了漏れ」が必ず起きます。

// src/hooks/useDataWorker.ts
import { useCallback, useEffect, useRef } from "react";
import type { WorkerJob, WorkerRequest, WorkerResponse } from "../workers/worker-protocol";

type PendingJob = {
  resolve: (value: unknown) => void;
  reject: (error: Error) => void;
};

export function useDataWorker() {
  const workerRef = useRef<Worker | null>(null);
  const pendingRef = useRef(new Map<string, PendingJob>());
  const nextIdRef = useRef(0);

  useEffect(() => {
    const worker = new Worker(new URL("../workers/data.worker.ts", import.meta.url), {
      type: "module",
    });

    workerRef.current = worker;

    worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
      const response = event.data;
      const pending = pendingRef.current.get(response.id);
      if (!pending) return;

      pendingRef.current.delete(response.id);
      if (response.ok) {
        pending.resolve(response.result);
      } else {
        pending.reject(new Error(response.error));
      }
    };

    worker.onerror = (event) => {
      for (const pending of pendingRef.current.values()) {
        pending.reject(new Error(event.message));
      }
      pendingRef.current.clear();
    };

    return () => {
      // 画面を離れたら、待機中のジョブを片付けてWorkerを終了する
      for (const pending of pendingRef.current.values()) {
        pending.reject(new Error("Worker was terminated"));
      }
      pendingRef.current.clear();
      worker.terminate();
      workerRef.current = null;
    };
  }, []);

  const runJob = useCallback(<T,>(job: WorkerJob, transfer: Transferable[] = []) => {
    const worker = workerRef.current;
    if (!worker) return Promise.reject(new Error("Worker is not ready"));

    const id = `worker-job-${Date.now()}-${nextIdRef.current}`;
    nextIdRef.current += 1;

    return new Promise<T>((resolve, reject) => {
      pendingRef.current.set(id, {
        resolve: resolve as (value: unknown) => void,
        reject,
      });

      worker.postMessage({ id, job } satisfies WorkerRequest, transfer);
    });
  }, []);

  return { runJob };
}

このhookは素のpostMessageを使っています。ComlinkというライブラリでRPC風に書く手もありますが、最初はメッセージの流れが目で追える実装のほうが、Claude Codeにレビューさせたときも理解しやすい。RPC風にしたくなったらComlinkのREADMEを見て、責務の分離は保ったまま置き換えればいいです。

UIから5種類の処理を呼ぶ

最後にコンポーネントです。CSV・検索・ログ・JSONは文字列や配列をそのまま渡します。画像だけはCanvasからImageDataを取り出し、バッファをtransferableとしてWorkerに渡しています。

// src/components/WorkerDemo.tsx
import { useRef, useState } from "react";
import { useDataWorker } from "../hooks/useDataWorker";
import type { CsvSummary, ImageResult, LogSummary, SearchIndex } from "../workers/worker-protocol";

const sampleCsv = `team,score,cost
alpha,91,1200
beta,84,950
gamma,96,1430`;

const sampleLogs = `2026-06-02T10:00:00Z INFO started
2026-06-02T10:01:00Z WARN cache miss
2026-06-02T10:02:00Z ERROR payment retry failed
2026-06-02T10:03:00Z ERROR payment retry failed`;

export function WorkerDemo() {
  const { runJob } = useDataWorker();
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const [message, setMessage] = useState("Idle");

  async function handleCsv() {
    const summary = await runJob<CsvSummary>({ type: "csv:summary", text: sampleCsv });
    setMessage(`CSV rows: ${summary.rows}, score average: ${summary.numeric.score.average.toFixed(1)}`);
  }

  async function handleSearch() {
    const index = await runJob<SearchIndex>({
      type: "search:index",
      documents: [
        { id: "a", title: "CSV reports", body: "Aggregate revenue and cost columns" },
        { id: "b", title: "Log monitor", body: "Find warning and error messages quickly" },
      ],
    });
    setMessage(`Search index tokens: ${Object.keys(index.tokens).length}`);
  }

  async function handleLogs() {
    const summary = await runJob<LogSummary>({ type: "log:summary", text: sampleLogs });
    setMessage(`Errors: ${summary.errors}, warnings: ${summary.warnings}`);
  }

  async function handleImage() {
    const canvas = canvasRef.current;
    const context = canvas?.getContext("2d");
    if (!canvas || !context) return;

    context.fillStyle = "#2f80ed";
    context.fillRect(0, 0, canvas.width, canvas.height);
    context.fillStyle = "#f2994a";
    context.fillRect(20, 20, 80, 80);

    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
    const buffer = imageData.data.buffer as ArrayBuffer;
    // 第2引数の [buffer] が「このバッファは移動する」という宣言
    const result = await runJob<ImageResult>(
      { type: "image:grayscale", width: imageData.width, height: imageData.height, buffer },
      [buffer],
    );

    context.putImageData(
      new ImageData(new Uint8ClampedArray(result.buffer), result.width, result.height),
      0,
      0,
    );
    setMessage("Image converted in worker");
  }

  async function handleJson() {
    const result = await runJob<Record<string, unknown>>({
      type: "json:flatten",
      value: { user: { id: 1, plan: "pro" }, flags: { beta: true } },
    });
    setMessage(`Flattened keys: ${Object.keys(result).join(", ")}`);
  }

  return (
    <section>
      <div>
        <button onClick={handleCsv}>Summarize CSV</button>
        <button onClick={handleSearch}>Build search index</button>
        <button onClick={handleLogs}>Analyze logs</button>
        <button onClick={handleImage}>Process image</button>
        <button onClick={handleJson}>Flatten JSON</button>
      </div>
      <p role="status">Worker finished: {message}</p>
      <canvas ref={canvasRef} width={160} height={120} aria-label="Image processing preview" />
    </section>
  );
}

派手なUIではありませんが、検証にはこれで十分です。大事なのは、ボタンを押したあともステータス表示・入力・スクロールが固まらないこと。差を体感したいなら、CSVは数万行、ログは数MB、検索は数千ドキュメントから試すと、Workerあり・なしの違いがはっきり見えます。

動いて見えても安心しない:検証の型

Workerは「TypeScriptの型が通る」だけでは終わりません。本当にUIが固まらないか、結果が返るか、画面を離れたあとちゃんと片付くか。ここまで見て初めて完成です。Playwrightの例はアプリに合わせてセレクタを直してください。

// tests/worker-demo.spec.ts
import { expect, test } from "@playwright/test";

test("heavy worker jobs finish without blocking the page", async ({ page }) => {
  await page.goto("http://localhost:5173/");

  await page.getByRole("button", { name: "Summarize CSV" }).click();
  await expect(page.getByRole("status")).toContainText("CSV rows");

  await page.getByRole("button", { name: "Build search index" }).click();
  await expect(page.getByRole("status")).toContainText("Search index tokens");

  await page.getByRole("button", { name: "Analyze logs" }).click();
  await expect(page.getByRole("status")).toContainText("Errors:");
});

自動テストだけだと「固まらないこと」は意外と拾えないので、僕は手動チェックも併用しています。Chrome DevToolsのPerformanceタブを開いて、大きいサンプルで各ボタンを押し、走らせている最中にスクロールと入力が生きているかを目で見る。さらに画面を離れてWorkerが消えるか、画像処理後に移動済みバッファを使い回していないかも確認します。

仕上げにClaude Code自身にもレビューさせます。観点を絞ると精度が上がります。

Review only the Web Worker implementation.

Find:
- code that touches DOM, window, or React state inside the worker
- untyped postMessage payloads
- missing terminate cleanup
- incorrect transferable usage
- bundler paths that fail in Vite
- responsibilities that should stay on the main thread

Return file paths, line numbers, and concrete fixes.

僕がハマった落とし穴

正直に書くと、最初にWorkerを入れたときはここに挙げる失敗をひととおりやりました。

1つ目は、WorkerからDOMを触ろうとしたこと。 Claude Codeがdocument.querySelectorをWorkerの中に書いてきて、実行時に落ちました。Workerは画面を直接更新できません。結果を返して、React側でstateを更新する。これが鉄則です。AIがWorker内でDOMを触ったら、その場で差し戻します。

2つ目は、postMessageの型崩れ。 type: stringpayload: anyで雑に始めたら、処理を足すたびに壊れて、どのメッセージが何を返すのか分からなくなりました。union typeで入力を固定し、戻り値もWorkerResultMapで管理する。最初の契約ファイルが効いてくるのはここです。

3つ目は、transferableの誤解。 大きいArrayBufferは移動できると知って喜んだものの、移動後の元バッファを使い回すコードを書いて、空のデータで画像が真っ黒になりました。移動したら元は捨てる。画像処理のあとに元のImageDataを再利用しない設計にします。

4つ目は、Worker終了漏れ。 画面を離れてもWorkerが残ると、メモリとCPUを裏で食い続けます。useEffectのcleanupでterminate()を呼び、待機中のPromiseもrejectしておく。地味ですが、ここを忘れると本番でじわじわ効いてきます。

5つ目は、バンドラ設定。 Viteではnew Worker(new URL("../workers/data.worker.ts", import.meta.url), { type: "module" })の形が安全です。文字列パスだけで書くと、本番ビルドやサブディレクトリ配信で読み込みに失敗することがあります。読み込み方はVite公式のWeb Workers解説が正確です。

6つ目は、責務の詰め込みすぎ。 WorkerにAPI呼び出し・通知・ルーティング・UI文言まで入れると、再利用もテストも一気に難しくなります。Workerは「重い純粋計算」、メインスレッドは「画面と副作用」。この線を守るだけで、後のメンテが別物になります。

よくある質問

Q. Web Workerを使えば処理は速くなりますか? A. 計算そのものは基本的に速くなりません。Workerの目的は「UIを止めないこと」です。計算は別スレッドで同じくらいの時間かかりますが、その間メイン側が空くので、体感(操作の快適さ)が劇的に良くなります。

Q. どのくらい重い処理から使うべきですか? A. 目安は「実行中にUIが一瞬でも固まる処理」です。数十件のソートや軽いフィルタはメインのままで十分。CSV数万行、画像のピクセル処理、数千件の検索インデックスあたりが効いてくるラインです。

Q. WorkerからAPIを呼んだりDBに書いたりできますか? A. fetchはWorkerからも使えますが、DOMやReact stateには触れません。設計としては、Workerは重い計算に専念させ、API呼び出しや画面更新といった副作用はメイン側に置くのがおすすめです。混ぜると一気にテストしづらくなります。

Q. ComlinkとどっちでWorker通信を書くべき? A. 学習中や小さい画面なら、まず素のpostMessageでメッセージの流れを把握するのがいいです。RPC風に書きたくなったらComlinkに移行する。その場合も「Workerは計算だけ」という責務分離は崩さないこと。

Q. Claude CodeにWorkerを書かせると何が一番危ないですか? A. Workerの制約(window/documentが無い)を無視した、もっともらしいコードです。だから最初のプロンプトで「Worker内ではDOM・window・React stateに触れない」と禁止事項を先に渡すのが効きます。

実際に試した結果

この構成を自分の画面で動かしてみて、いちばん効いたのは「責務を先に言葉で決めてからClaude Codeに渡す」ことでした。CSV集計・検索インデックス・ログ解析は素のpostMessageで問題なく扱えて、Workerあり・なしでスクロールの止まり方がはっきり変わりました。

一方で画像処理は、transferableを使うかどうかで体感が大きく変わるポイントで、ここがレビューでいちばん価値が高かった。コピーで渡していた頃は、結局メイン側が一瞬詰まっていたんです。移動に変えたら、その引っかかりが消えました。

冒頭の「数万行CSVでフリーズ」も、計算をWorkerに移しただけで解決しました。速くなったわけではなく、止まらなくなった。この違いが分かると、Web Workerの使いどころを外さなくなります。公開前のセルフチェックには、コードフェンスの数・本文量・内部/外部リンク・updatedDateを毎回入れています。

重いフロントエンド処理の切り出しをチームの開発フローに組み込みたい場合は、Claude Code研修・導入相談で実リポジトリを題材に一緒に整理できます。まず手を動かして地力をつけたい人は、Claude Code生産性Tipsも合わせてどうぞ。

#Claude Code #Web Worker #UIフリーズ #パフォーマンス #TypeScript
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。