メインコンテンツへスキップ
ブログ一覧に戻る
Web制作・運用

JavaScript/CSS最適化完全ガイド:パフォーマンスを実質的に改善する実践手法

2025年11月17日
最終更新: 2026年10月8日
9分で読めます

この記事でわかること

JavaScriptとCSSの最適化を、転送量・初回表示・操作中の処理に分けて解説します。遅延読み込み、圧縮、Tree Shaking、Critical CSS、scriptの順序をコードで学び、架空の削減率と実測結果を区別します。変更前後の条件、失敗時の表示、CSSを残す判断まで、検証の進め方を具体化します。

2026年10月8日に、Criticalの現行公式READMEを再読しました。Criticalはabove-the-foldのcritical-path CSSを抽出してインライン化し、残りを非同期で読み込む用途を示し、DOMに合わせる方法と実ビューポートを使う方法、npm install --save-dev criticalの導入例を案内しています。これはツールの処理範囲であり、対象ページの速度改善、CSP・キャッシュとの整合、抽出CSSの正しさ、Core Web Vitalsの改善を自動的に証明するものではありません。

30秒で要点

  • JavaScriptとCSSは、転送量・初回表示・操作中の処理を別々に測ります。圧縮でファイルが小さくなっても、実行する仕事が同じなら操作待ちは残ります。
  • 最初に不要な依存と初回に不要な画面を分け、次に圧縮・CSSの残し方・読み込み順を確認します。分割したファイルを直ちに全部描画すれば、遅延読み込みの効果は限られます。
  • 以下のコードは用途別の教材です。実ブラウザや本番ビルドを通した速度改善率を示すものではありません。採用する構成に合う例だけを使い、表示・操作・失敗時の動作を検証します。

「500KBを100KBにできたので表示も80%速くなる」とは限りません。架空の転送量なら (500 - 100) / 500 = 80% の削減ですが、処理時間や売上の分母とは別です。120msの処理を40msずつに分けても、仕事の合計は120msです。処理の間にブラウザへ制御を返せるかが、操作待ちには重要です。長いタスクの説明では、50msを超えるタスクと、その分割を区別しています。

転送量500KBから100KBの80%削減と、120msの仕事を40msずつに分ける時間軸を別々に示す架空例
著者作成。量の80%削減を時間の80%短縮へ読み替えません。分割した仕事の合計は同じ120msです。

測定から変更を一つ選ぶ

表が収まらない場合は、横にスクロールできます

観測する問題最初に見るもの変更候補変更後の確認
初回転送が多いページで実際に使う依存・圧縮後サイズ機能単位の分割・重複除去必要な画面を開いた際の追加通信
文字が出るまで待つ初期HTMLと描画に必要なCSSCSSの適用順・必要部分の抽出未装飾表示・画面幅別の崩れ
ボタンの応答が遅いメインスレッドの処理区間不要処理の削除・仕事の分割同じ操作と端末条件での応答
変更後に装飾が消えた副作用のあるCSS importsideEffectsの宣言訂正本番相当の最適化後の見た目

まず同じURL、画面幅、操作、キャッシュ状態を記録し、変更を一つだけ入れます。転送量の改善と操作応答の改善は別に記録してください。HTMLの到着自体が遅い場合は、TTFBの調査から始める方が適切です。

1. 必要になる時点まで読み込みを遅らせる

Next.jsのdynamicとルートの分割

App RouterのClient Componentの例です。dynamicはモジュールの外側で宣言し、利用者が開くまでは重いコンポーネントを描画しません。部品側のdefault exportと、失敗時に再読込を案内するError Boundaryも用意します。ssr: falseはSSRできない部品に理由がある場合の選択で、速度向上の万能指定ではありません。Next.jsの遅延読み込みに対応する範囲の例です。

'use client';
import dynamic from 'next/dynamic';
import { useState } from 'react';
const Report = dynamic(() => import('./Report'), {
  loading: () => <p role="status">集計画面を読み込んでいます</p>,
});
export default function Reports() {
  const [open, setOpen] = useState(false);
  return <><button onClick={() => setOpen(true)}>集計を開く</button>
    {open && <Report />}</>;
}

Pages Routerでは、ページファイルごとのルート境界を利用できます。次は構成の教材であり、App Routerのapp/と混ぜる指示ではありません。共通レイアウトに重い管理画面の依存を持ち込むと、ルートを分けても初回負担が残ります。

pages/
  index.tsx           # 一覧に必要な部品だけ
  reports.tsx         # 集計の依存はこのルートへ
components/
  Report.tsx          # default export

Reactだけの構成ではlazyとSuspenseを使えます。lazyは初回の描画要求で読み込みを始めるため、最初からChartを描画すると利用者の操作まで待つことにはなりません。Reactの契約ではPromiseと解決結果が再利用され、読み込み失敗はError Boundaryへ渡されます。

import { lazy, Suspense, useState } from 'react';
const Chart = lazy(() => import('./Chart'));
export function ChartPanel() {
  const [show, setShow] = useState(false);
  return <><button onClick={() => setShow(true)}>グラフを表示</button>
    {show && <Suspense fallback={<p>グラフを準備中</p>}>
      <Chart />
    </Suspense>}</>;
}

圧縮は転送と実行を分けて考える

webpackを直接管理する構成の設定例です。Next.jsなどが管理するビルドへ、そのまま追加しません。必要なプラグインのバージョンを固定し、ソースマップの扱いと本番出力を確認します。

// webpack.config.cjs:必要なパッケージを導入した環境向け
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
  },
};

minifyで空白などを減らす処理と、HTTPのgzip圧縮は別です。次はnginxの設定教材です。実サーバーへ適用した記録ではありません。上流CDNが圧縮する構成では、どこがContent-EncodingとVaryを付けるかを確認します。圧縮対象、CPU負荷、機密情報を含む応答の扱いも検討します。nginx公式仕様を設定の根拠にしてください。

# http または対象 server の管理方針に合わせて配置する例
gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json;

Brotliも「常に一定割合よくなる」というものではありません。以下はNode.jsの圧縮APIで同じ入力を比較し、復元一致を確かめるローカル教材です。HTTPの配信設定、ブラウザ対応、CDNのキャッシュキーまでは検証しません。

import assert from 'node:assert/strict';
import { gzipSync, gunzipSync, brotliCompressSync, brotliDecompressSync } from 'node:zlib';
const source = Buffer.from('export const message = "hello";\n'.repeat(200));
const gzip = gzipSync(source);
const brotli = brotliCompressSync(source);
assert.deepEqual(gunzipSync(gzip), source);
assert.deepEqual(brotliDecompressSync(brotli), source);
console.log({ originalBytes: source.length, gzipBytes: gzip.length, brotliBytes: brotli.length });

2. 未使用コードと副作用を分ける

ES Modulesの静的なimport/exportは、依存解析に利用できます。ただし、CommonJSなら必ず全量になる、ESMなら必ず削除できる、という二択ではありません。バンドラーと配布形式に依存します。

// 選択肢A:CommonJSのAPI利用
// const { format } = require('./utilities.cjs');
// 選択肢B:ESMのAPI利用(utilities.mjsがformatをexportする)
import { format } from './utilities.mjs';
console.log(format('sample'));

次は未使用exportの解析を有効にする設定例です。解析結果が実際に削除されるかはminifyなどの後段も確認します。

export default {
  mode: 'production',
  optimization: { usedExports: true },
};

sideEffects: falseを無条件で付けると、CSS importまで除去される場合があります。webpackのCSS例が示すように、副作用を持つファイルは残します。次の宣言ならCSSと初期化ファイルを残す意図を表せますが、実際のファイル構成と一致させてください。

{
  "sideEffects": ["**/*.css", "./src/register-polyfills.js"]
}

依存の大きさは分析用ビルドで確認します。次はwebpack-bundle-analyzerを導入済みの場合の例です。静的レポートを開くだけで、未使用判定や改善の完了にはなりません。

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
  plugins: [new BundleAnalyzerPlugin({
    analyzerMode: 'static', openAnalyzer: false,
    reportFilename: 'bundle-report.html',
  })],
};

共通依存を別チャンクにする設定も、細かく分けるほど良いとは限りません。変更頻度と再訪時のキャッシュ利用を見ます。

export default {
  optimization: { splitChunks: {
    chunks: 'all',
    cacheGroups: { vendor: {
      test: /[\\/]node_modules[\\/]/,
      name: 'vendor', reuseExistingChunk: true,
    } },
  } },
};

3. CSSは「取り出す」と「残りを適用する」を一組にする

Critical CSSは初期表示に必要な規則を抽出する考え方です。全画面幅・ログイン状態・遅れて表示する部品を一枚の静的HTMLだけで網羅できるとは限りません。生成されたCSSをレビューし、残りのスタイルも確実に適用します。

以下は現在のcriticalのREADMEで確認したcritical関数による設定例です。古いgenerate APIをそのまま新しい版の契約として扱いません。パッケージ導入と実ページ描画はこの教材のローカル検証範囲外です。

import { critical } from 'critical';
await critical({
  base: 'dist/', src: 'index.html',
  width: 390, height: 844, inline: true,
  target: { html: 'index-critical.html' },
});

次は「必要部分を先に渡し、全体のCSSも残す」構造例です。全CSSを通常のstylesheetにしたため、その部分は描画を待たせ得ます。preloadだけを書いてもスタイルは適用されません。非同期適用を選ぶ場合は、CSP、JavaScript無効、読み込み失敗、未装飾表示まで設計します。

<style nonce="SERVER_NONCE">.hero{min-height:18rem}.hero h2{font-size:2rem}</style>
<link rel="stylesheet" href="/styles/app.css">

Pages Routerの独自Documentでも同じ区別が必要です。nonceは毎応答のCSPと一致する値をサーバー側から渡す必要があり、下の固定文字列を本番のnonceにしません。フレームワーク管理のCSSを重複挿入しないことも確認します。

import Document, { Html, Head, Main, NextScript } from 'next/document';
export default class MyDocument extends Document {
  render() {
    // CSS生成・CSP接続を別途設計した場合だけ配置する教材
    return <Html lang="ja"><Head>
      <link rel="stylesheet" href="/styles/landing.css" />
    </Head><body><Main /><NextScript /></body></Html>;
  }
}

架空の200KBのCSSから20KBをHTMLへ移しても、残り180KBを含めるなら合計は200KBです。転送の削減ではなく配信時点の変更です。HTMLのキャッシュと重複CSSも含めて比較します。

4. scriptの順序と失敗を設計する

外部のclassic scriptを順序どおりDOM解析後に実行したい例です。deferは実行時の重い処理そのものを消しません。

<script defer src="/scripts/vendor.js"></script>
<script defer src="/scripts/page.js"></script>

asyncは取得完了時に実行され、複数scriptの実行順は保証されません。依存順序が不要な独立した処理に限定します。module scriptの既定動作やインラインscriptは別条件なので、script要素の仕様と照合します。

<script async src="/scripts/independent-widget.js"></script>

操作後に同じscriptを二重挿入しないための教材です。失敗時には登録を削除して再試行可能にし、許可済み同一オリジンURLだけを呼出側から渡します。利用者入力を無検証で渡す用途ではありません。

export function createScriptLoader(doc) {
  const pending = new Map();
  return function loadScript(src) {
    if (pending.has(src)) return pending.get(src);
    const promise = new Promise((resolve, reject) => {
      const element = doc.createElement('script');
      element.src = src;
      element.onload = () => resolve(element);
      element.onerror = () => { element.remove(); reject(new Error('load failed')); };
      doc.head.appendChild(element);
    }).catch(error => { pending.delete(src); throw error; });
    pending.set(src, promise);
    return promise;
  };
}

SPAでもルートごとに読み込む部品を分けます。次はルーター本体を省いた表示の教材で、URL変更・履歴・404処理は実際のルーターへ接続する必要があります。

import { lazy, Suspense } from 'react';
const Help = lazy(() => import('./Help'));
const Home = lazy(() => import('./Home'));
export function RouteView({ route }: { route: 'home' | 'help' }) {
  return <Suspense fallback={<p>画面を準備中</p>}>
    {route === 'help' ? <Help /> : <Home />}
  </Suspense>;
}

ECページを一つ改善する練習

架空の商品ページで、初回は商品名と画像が必要、レビュー集計は利用者が開いたときだけ必要、と整理します。まず集計の依存を初回から外し、商品画像のサイズと画像最適化を確認します。次は初期表示の骨組みです。インラインCSSだけで全体が完成する例ではありません。

<style>.product{max-width:64rem;margin:auto}.product img{max-width:100%;height:auto}</style>
<main class="product">
  <h1>架空の商品A</h1>
  <img src="/images/product-a.webp" width="800" height="600" alt="商品Aの正面">
  <button type="button" id="open-reviews">レビュー集計を開く</button>
</main>
<link rel="stylesheet" href="/styles/product.css">

変更票には「集計を開くまで取得を遅らせる」「初回転送量を同条件で比較」「開く操作の待ち時間・失敗表示を確認」「使えなければ元の配信へ戻す」と記します。未使用率だけで削除せず、注文・検索・戻る操作も試してください。業務成果を評価するなら、性能とは別に観測期間、対象流入、分母、同時変更を記録します。

学習順序はプログラミングの基礎、Web制作入門、現代の開発構成から選べます。設計上の位置づけはWebサイト設計、計測からの全体手順はWebパフォーマンスを参照してください。

よくある質問(FAQ)