クライアント側のサーバー(へテムル)で、モバイルのPageSpeed Insightsスコアが14点という衝撃的な数値だったため(カラフルボックスでは55点)、Webパフォーマンス改善に取り組むことにしました。
結果として、以下の改善を達成できました。
| 指標 | Before | After | 改善率 |
|---|---|---|---|
| スコア | 14 | 52 | +38pt |
| TBT | 1,240ms | 430ms | -65% |
| CLS | 0.423 | 0 | 完全解消 |
| FCP | 5.0秒 | 2.9秒 | -42% |
この記事では、実際に効果があった5つの施策を、具体的なコード例とともに解説します。
1. document.write の廃止
問題点document.write を使ったスクリプトの動的読み込みは、レンダリングをブロックする最悪のパターンです。
// ❌ 悪い例:document.write によるスクリプト読み込み
document.write('<script src="/js/script1.js"></script>');
document.write('<script src="/js/script2.js"></script>');
このコードには、
- 同期的に実行される:HTMLパースが完全に停止する
という問題があり、早く改善したかったのですが、
document.write だと動くのに、Claudeさんに書いてもらったコードだとライブラリが動かない、動かない。とにかく動かないとWebパフォーマンス改善以前の状態なので、document.write にしておくしかない。
原因が document.body.appendChild(script); にあり、document.body.appendChild だと、その時点ではまだbody要素がないから動かないということに気づくのに結構な時間がかかりました。
HTMLパース開始
↓
<head> 処理
↓
★ <head> 内 のスクリプト実行
(※<body>解析がまだなので、document.body.appendChild は機能しない)
// ✅ 良い例:動的スクリプト読み込み
/**
* 非同期読み込み(順序不定)
* 用途:独立したスクリプト、他に依存しないもの
*/
function loadScriptAsync(src, callback) {
const script = document.createElement('script');
script.src = src;
script.async = true;
if (callback) script.addEventListener('load', callback);
document.body.appendChild(script);
}
/**
* 順序保証読み込み
* 用途:依存関係があるスクリプト(A→B→Cの順で実行したい場合)
*/
function loadScriptOrdered(src, callback) {
const script = document.createElement('script');
script.src = src;
script.async = false; // 順序保証
if (callback) script.addEventListener('load', callback);
document.body.appendChild(script);
}
ポイント: async = false を設定すると、スクリプトの実行順序が保証されます。
2. Critical CSS の導入
問題点改善中、スクロールアニメーション(scroll-shrinkIn)やSwiperスライダーのCSSが読み込みが間に合わず、
- FOUC(Flash of Unstyled Content): 一瞬スタイルが適用されていない状態が見える
- CLS(Cumulative Layout Shift): レイアウトがガタッとずれる
という問題が発生しました。(ライブラリの読み込みが間に合っていない)
解決策「クリティカルCSSジェネレーター」を作成して、Above the fold(ファーストビュー)に必要な最小限のCSSをCritical CSSとして先に読み込むようにしました。
critical-css-generator | CODE-PLUS(コードプラス)
WebページのCritical CSS(Above the fold に必要な最小限のCSS)を抽出するオンラインツール。ページ表示速度の改善、Core Web Vitals(FCP, LCP)のスコア向上に。例)critical.min.css
/* scroll-shrinkIn 初期状態 */
.scroll-shrinkIn {
transform: scale(1.1);
}
@keyframes scrollShrinkIn {
to {
transform: scale(1);
}
}
.scroll-shrinkIn.is-scloaded {
animation-name: scrollShrinkIn;
}
/* Swiper 初期状態(スライドずれ防止) */
.swiper {
position: relative;
overflow: hidden;
}
.swiper-wrapper {
display: flex;
transition-property: transform;
}
.swiper:not(.swiper-initialized) .swiper-slide {
visibility: hidden;
}
.swiper:not(.swiper-initialized) .swiper-slide:first-child {
visibility: visible;
}
読み込み方法:
<!-- head内の先頭で読み込み -->
<link rel="stylesheet" href="/css/critical.min.css">
この対策により、CLS が 0.423 → 0 に改善しました。
3. 優先度の低いCSSの遅延読み込み
問題点すべてのCSSをheadで読み込むと、レンダリングブロックが発生し、FCPが遅くなります。
解決策Above the foldに不要なCSSは、JavaScriptで遅延読み込みします。
/**
* Lazy CSS読み込み(Below the fold用)
*/
function loadLazyCSS(href) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
document.head.appendChild(link);
}
// 使用例:footerで読み込み
loadLazyCSS('/css/print-style.css');
loadLazyCSS('https://fonts.googleapis.com/css2?family=Shippori+Mincho&display=swap');
遅延読み込みに適したCSS:
- 印刷用スタイル
- Below the fold(スクロール後に表示される領域)のスタイル
- 装飾的なWebフォント
- シンタックスハイライター(Prism等)のスタイル
4. headerフェードインでFOUT対策
問題点改善中、Webフォント(特に日本語フォント)の読み込みに時間がかかり、一瞬だけヘッダーで別のフォントで表示される FOUT(Flash of Unstyled Text) が発生しました。(フォントの読み込みが間に合っていない)
解決策headerを最初は非表示にし、フォントが読み込まれる頃にフェードインさせます。
/* header フェードイン */
@keyframes headerIn {
100% {
opacity: 1;
}
}
.header {
opacity: 0;
animation: headerIn 0.5s ease-out 0.5s;
animation-fill-mode: forwards;
}
ポイント:
animation-delay: 0.5sでフォント読み込みの時間を稼ぐ
これにより、一瞬でも別のフォントで表示されるということなく、優先度の低いフォントを遅延読み込みすることができます。
5. load / requestIdleCallbackメソッド でJS遅延読み込み
問題点すべてのJavaScriptを即時実行すると、TBT(Total Blocking Time)が増加し、インタラクティブになるまでの時間が長くなります。
解決策スクリプトを優先度別に分類し、適切なタイミングで読み込みます。
/**
* ブラウザのアイドル時に実行
* 用途:ユーザー操作後で十分な優先度の低いスクリプト
*/
function loadWhenIdle(callback) {
if ('requestIdleCallback' in window) {
requestIdleCallback(callback, { timeout: 2000 });
} else {
// フォールバック:load後に実行
window.addEventListener('load', callback);
}
}
//========================================
// ▼【即時】レイアウト関連(順序保証)
//========================================
loadScriptOrdered('/js/_enchants/_classify-cabinetmaker.js');
loadScriptOrdered('/js/_enchants/_classify-floatBottom.js');
//========================================
// ▼【load後】表示・アニメーション関連
//========================================
window.addEventListener('load', function() {
loadScriptAsync('/js/_enchants/_link-animeFade.js');
loadScriptAsync('/js/_enchants/_link-animeSmooth.js');
loadScriptAsync('/js/desvg/desvg.custom.min.js', function() {
deSVG('.desvg', true);
});
});
//========================================
// ▼【アイドル時】ユーザー操作・補助機能
//========================================
loadWhenIdle(function() {
loadScriptAsync('/js/_enchants/_unlock-checkkey.js');
loadScriptAsync('/js/_enchants/_be-copyable.js');
loadScriptAsync('/libraries/ajaxzip3/ajaxzip3.js');
});
読み込みタイミングの判断基準
| タイミング | 用途 | 例 |
|---|---|---|
| 即時実行 | レイアウトに影響するもの | Masonry、高さ計算系 |
| load後 | 表示・アニメーション関連 | リンク、画像.svgのインラインSVG変換 |
| アイドル時 | ユーザー操作後で十分なもの | フォーム補助、クリップボード |
この分類により、TBT が 1,240ms → 430ms(65%削減)を達成しました。
イベントタイミングの理解
JavaScriptの読み込みタイミングを最適化するには、ブラウザのイベント順序を理解することが重要です。
HTMLパース開始
↓
<head> 処理
↓
★ <head> 内 のスクリプト実行
(※<body>解析がまだなので、document.body.appendChild は機能しない)
↓
<body> 解析
↓
</body> 直前 ← footでのスクリプト実行
↓
★ DOMContentLoaded 発火(DOM構築完了)
↓
画像・CSS・iframe等 すべて読み込み完了
↓
★ load 発火
↓
ブラウザがアイドル状態になったとき
↓
★ requestIdleCallback 発火
注意: </body> 直前でスクリプトを実行する場合、DOMContentLoaded を待つ必要はありません(既にDOMは構築済みのため)。
まとめ
| 施策 | 主な効果 |
|---|---|
| document.write 廃止 | レンダリングブロック解消 |
| Critical CSS 導入 | CLS 0.423 → 0 |
| headerフェードイン | FOUT解消 |
| CSS遅延読み込み | FCP改善 |
| JS遅延読み込み | TBT 65%削減 |
最も効果が大きかったのは、document.writeの廃止。requestIdleCallbackメソッド によるJS遅延読み込み も予想以上に効果がありました。(一方、JSの結合・バンドル化はほとんど効果なし)。
ユーザー操作後で十分なスクリプト(フォーム補助、クリップボード機能など)をアイドル時に遅延させることで、初期表示のパフォーマンスを大幅に改善できます。
今回はCSS・JSファイルの読み込みに関するWebパフォーマンス改善について主にご紹介しました。本当は速いサーバーに変更するのが、最も効果が大きいとは思いますが…。

