JavaScriptを遅延またはスケジュールする16の方法
JavaScriptを遅延させ、スケジュールする方法を学びます。
なぜJavaScriptをdeferまたはスケジュール実行するのか?
JavaScriptは、いくつかの方法でウェブサイトの速度を低下させます(そして実際に低下します)。これはCore Web Vitalsにさまざまな悪影響を及ぼします。JavaScriptはネットワークリソースを奪い合い、CPUリソースを奪い合ってmain threadをブロックし、さらにはページの解析をブロックすることさえあります。スクリプトのdeferやスケジュール実行を行うことで、Core Web Vitalsを劇的に改善できます。
最終確認:Arjen Karel(2026年2月)
Table of Contents!
- なぜJavaScriptをdeferまたはスケジュール実行するのか?
- JavaScriptの実行タイミングはCore Web Vitalsにどのように影響するのか?
- 適切なdefer方法をどのように選ぶか?
- 方法1:defer属性を使用する
- 方法2:async属性を使用する
- 方法3:モジュールを使用する
- 方法4:スクリプトをページ下部付近に配置する
- 方法5:スクリプトを動的に挿入する
- 方法6:スクリプトを後から動的に挿入する
- 方法7:スクリプトのtype属性を変更する(その後、元に戻す)
- 方法8:Intersection Observerを使用する
- 方法9:readystatechangeを使用する
- 方法10:タイムアウトなしのsetTimeoutを使用する
- 方法11:タイムアウトを指定したsetTimeoutを使用する
- 方法12:Promiseを使用してマイクロタスクを設定する
- 方法13:マイクロタスクを使用する
- 方法14:requestIdleCallbackを使用する
- 方法15:postTaskを使用する
- 方法16:scheduler.yield()を使用する
JavaScriptがCore Web Vitalsに与える悪影響を最小限に抑えるには、スクリプトがダウンロードキューに入るタイミングを指定し、CPU時間を占有してmain threadをブロックするタイミングを スケジュール するのが通常は効果的です。
JavaScriptの実行タイミングはCore Web Vitalsにどのように影響するのか?
JavaScriptの実行タイミングはCore Web Vitalsにどのように影響するのでしょうか。実際の例を見てみましょう。1つ目のページは、'render blocking'なJavaScriptを含んで読み込まれています。ペイント指標とTotal Blocking Timeは非常に悪い状態です。2つ目の例はまったく同じページですが、JavaScriptをdeferにしています。LCPの画像は依然として大きな影響を受けていることがわかります。3つ目の例は、同じスクリプトをページの「load」イベントの後に実行し、関数呼び出しを小さく分割したものです。この最後の例は、余裕を持ってCore Web Vitalsをクリアしています。



デフォルトでは、ページのhead内にある外部JavaScriptはレンダリングツリーの作成をブロックします。具体的には、ブラウザはドキュメント内でスクリプトを検出すると、DOM構築を一時停止し、JavaScriptランタイムに制御を移してスクリプトを実行した後に、DOM構築を再開します。これはペイント指標(Largest Contentful PaintおよびFirst Contentful Paint)に影響します。
deferまたはasyncを指定したJavaScriptも、ペイント指標(特にLargest Contentful Paint)に影響を与える可能性があります。DOMが作成された後にスクリプトが実行され、main threadをブロックするためです(この時点で、画像などの一般的なLCP要素のダウンロードが完了していない場合があります)。
外部JavaScriptファイルは、ネットワークリソースの競合も引き起こします。通常、外部JavaScriptファイルは画像よりも先にダウンロードされます。読み込むスクリプトが多すぎると、画像のダウンロードが遅れることになります。
最後に、JavaScriptはユーザーの操作(インタラクション)をブロックまたは遅延させる可能性があります。スクリプトがCPUリソースを使用している(main threadをブロックしている)とき、ブラウザはそのスクリプトが完了するまで入力(クリックやスクロールなど)に反応しません。これは、Interaction to Next Paint (INP)のスコアに直接影響します。
この影響は測定可能です。2025 Web Almanacによると、render-blocking resourcesの監査に合格しているモバイルページはわずか15%です。モバイルにおけるTotal Blocking Timeの中央値は1,916ミリ秒です。これは、ブラウザがユーザー入力に応答できない時間がほぼ丸2秒あることを意味します。各スクリプトに適切なdefer方法を選択することで、この数値を下げることができます。
JavaScriptのスケジュール実行やdeferは、どのようにCore Web Vitalsを改善するのでしょうか?
JavaScriptのスケジュール実行やdefer自体が直接Core Web Vitalsを解決するわけではありません。重要なのは、状況に応じて適切な手法を使い分けることです。原則として、スクリプトの実行遅延は最小限に留め、適切なタイミングでダウンロードキューに登録し、実行するようにします。
適切なdefer方法をどのように選ぶか?
スクリプトはすべて同じではなく、それぞれ異なる機能を持っています。レンダリングプロセスの初期段階で読み込むことが重要なスクリプトもあれば、そうでないものもあります。
重要度に基づいて、JavaScriptを4つのグループに分類しています。
1. Render critical. ページの見た目を変更するスクリプトです。読み込まれないとページが未完成に見えます。このようなスクリプトは極力避けるべきです。何らかの理由で回避できない場合は、deferすべきではありません。例えば、トップスライダーやA/Bテストのスクリプトがこれに該当します。
2. Critical。ページの見た目を(ほとんど)変更しませんが、これらがないとページが正常に機能しません。これらのスクリプトはdeferまたはasyncにする必要があります。例えば、メニューのスクリプトなどです。
3. Important。サイト管理者または訪問者にとって価値があるため、読み込みたいスクリプトです。私は通常、これらのスクリプトをloadイベントが発生した後に読み込みます。例えば、アナリティクスや「トップへ戻る」ボタンなどです。
4. Nice to have。最悪の場合、なくても困らないスクリプトです。これらのスクリプトは最も低い優先度で読み込み、ブラウザがアイドル状態のときにのみ実行します。例えば、チャットウィジェットやFacebookのボタンなどです。
方法1:defer属性を使用する
defer属性を持つスクリプトは並行してダウンロードされ、defer JavaScriptキューに追加されます。ブラウザがDOMContentLoadedイベントを発生させる直前に、そのキュー内のすべてのスクリプトがドキュメント内の出現順に実行されます。
<script defer src='javascript.js'></script>
この『deferのテクニック』は通常、多くの問題、特にペイント指標を解決します。しかし、残念ながら保証はなく、スクリプトの品質に依存します。deferされたスクリプトは、すべてのスクリプトが読み込まれ、HTMLの解析が完了した時点(DOMContentLoaded)で実行されます。その時点でLCP要素(通常は大きな画像)がまだ読み込まれていない場合、deferされたスクリプトの実行によってLCPが遅延する可能性があります。
使用すべきケース:
できるだけ早く必要とされるCriticalなスクリプトに、deferスクリプトを使用します。
メリット:
- deferされたスクリプトは並行してダウンロードされます
- 実行時にDOMを利用できます
デメリット:
- deferされたスクリプトがLCP指標を遅延させる可能性があります
- deferされたスクリプトは実行時にmain threadをブロックします
- インラインスクリプトやasyncスクリプトが依存している場合、スクリプトをdeferにするのは安全ではない可能性があります
方法2:async属性を使用する
async属性を持つスクリプトは並行してダウンロードされ、ダウンロードが完了するとすぐに実行されます。
<script async src='javascript.js'></script>
asyncスクリプトは、ページ速度の問題をほとんど解決しません。並行してダウンロードされるのは素晴らしいことですが、それだけです。ダウンロードが完了すると、実行時にmain threadをブロックします。
使用すべきケース:
できるだけ早く必要であり、かつ自己完結している(他のスクリプトに依存しない)Criticalなスクリプトに、asyncスクリプトを使用します。
メリット:
- asyncスクリプトは並行してダウンロードされます。
- asyncスクリプトはできるだけ早く実行されます。
デメリット:
- DOMContentLoadedは、asyncスクリプトの実行前と実行後のどちらでも発生する可能性があります。
- スクリプトの実行順序を事前に知ることはできません。
- 他のasyncスクリプトやdeferスクリプトに依存するasyncスクリプトは使用できません。
これら2つのアプローチの詳細な比較については、deferとasyncの比較、およびこれがCore Web Vitalsに与える影響を参照してください。
方法3:モジュールを使用する
モジュールスクリプトは、async属性がない限り、デフォルトでdeferされます。async属性がある場合は、asyncスクリプトと同様に処理されます。
<script module src='javascript.js'></script>
モジュールはJavaScriptの新しい考え方であり、いくつかの設計上の欠陥を修正します。それ以外に、スクリプトモジュールを使用してもウェブサイトは高速化されません。
使用すべきケース:
アプリケーションがモジュール式で構築されている場合、JavaScriptモジュールを使用するのが合理的です。
メリット:
- モジュールはデフォルトでdeferされます
- モジュールは保守が容易で、モジュール式ウェブデザインと相性が抜群です
- モジュールを使用すると、動的インポートによるコード分割が容易になり、必要なときに必要なモジュールだけをインポートできます
デメリット:
- モジュール自体がCore Web Vitalsを改善するわけではありません
- オンデマンドや実行時にモジュールをインポートすると、処理が遅くなり、INPを悪化させる可能性があります
方法4:スクリプトをページ下部付近に配置する
フッターのスクリプトは、後回しでダウンロードキューに入ります。これにより、ドキュメント内のスクリプトタグより上にある他のリソースが優先されます。
<html>
<head></head>
<body>
[ここにページのコンテンツが入ります]
<script defer src='javascript.js'></script>
</body>
</html>
スクリプトをページ下部に配置するのは興味深い手法です。これにより、他のリソース(画像など)がスクリプトよりも先にスケジュールされます。結果として、JavaScriptファイルのダウンロードが完了し、スクリプト実行によってmain threadがブロックされる前に、ブラウザが画像などを取得して画面に描画できる確率が高まります。それでも、保証はありません。
使用すべきケース:
スクリプトのパフォーマンスはすでに十分に良好であるものの、画像やウェブフォントなどの他のリソースを少し優先させたい場合に使用します。
メリット:
- スクリプトをページ下部に配置するのに、高度な知識は必要ありません。
- 正しく行えば、ページが破損するリスクはありません。
デメリット:
- Criticalなスクリプトのダウンロードと実行が遅れる可能性があります
- 根本的なJavaScriptの問題は解決しません
方法5:スクリプトを動的に挿入する
動的に挿入されたスクリプトは、asyncスクリプトと同様に扱われます。並行してダウンロードされ、ダウンロード完了後すぐに実行されます。
<script>
const loadScript = (scriptSource) => {
const script = document.createElement('script');
script.src = scriptSource;
document.head.appendChild(script);
}
// 'javascript.js'を動的挿入するloadScript関数を呼び出す
loadScript('javascript.js');
</script>
Core Web Vitalsの観点からは、この手法は<script async>を使用するのとまったく同じです。
使用すべきケース:
この方法は、できるだけ早く起動するサードパーティスクリプトでよく使用されます。関数呼び出しにより、コードのカプセル化や圧縮が容易になります。
メリット:
- asyncスクリプトを挿入するコードが自己完結すること。
デメリット:
- DOMContentLoadedは、スクリプトの読み込み前と読み込み後のどちらでも発生する可能性があります。
- スクリプトの実行順序を事前に知ることはできません。
- 他のasyncスクリプトやdeferスクリプトに依存するスクリプトには使用できません
方法6:スクリプトを後から動的に挿入する
私の考えでは、Nice to haveなスクリプトは決してdeferで読み込むべきではありません。最も適切なタイミングで動的挿入すべきです。以下の例では、ブラウザが『load』イベントを発行した後にスクリプトが実行されます。
<script>
window.addEventListener('load', function () {
// loadScript関数については方法5を参照
loadScript('javascript.js');
});
</script>
これは、Largest Contentful Paintを確実に改善できる最初の手法です。ブラウザが『load』イベントを発行した時点では、画像を含むすべての重要なリソースがダウンロード済みになっています。ただし、loadイベントが発生するまでに長い時間がかかる場合があり、別の問題が生じる可能性があります。
使用すべきケース:
ペイント指標に影響を与える理由が一切ない、Nice to haveなスクリプトに使用します。
メリット:
- ページとそのリソースの読み込みが完了した後にスクリプトを挿入するため、重要リソースと競合しません
デメリット:
- Core Web Vitalsの観点からページの設計が不十分な場合、ページが「load」イベントを送信するまでに長い時間がかかる可能性があります
- これをCriticalなスクリプト(lazy loadingやメニューなど)に適用しないよう注意する必要があります
方法7:スクリプトのtype属性を変更する(その後、元に戻す)
ページ上に、1. type属性があり、2. そのtype属性が"text/javascript"以外であるスクリプトタグが存在する場合、ブラウザはそのスクリプトをダウンロードも実行もしません。多くのJavaScriptローダー(Cloudflare of Rocket Loaderなど)はこの原理を利用しています。この仕組みは非常にシンプルでスマートです。
まず、すべてのスクリプトが次のように書き換えられます:
<script type="some-cool-script-type" src="javascript.js"></script>
その後、読み込みプロセスのどこかの時点で、これらのスクリプトは『通常のJavaScript』に戻されます。
使用すべきケース:
この手法は推奨しません。JavaScriptによる影響の改善は、すべてのスクリプトを実行キューの少し後ろに動かすだけで済むほど単純ではないからです。一方で、ページ上で実行されているスクリプトをほとんど制御できない場合や、十分なJavaScriptの知識がない場合には、これが最善の策となるかもしれません。
メリット:
- 簡単です。Rocket Loaderなどのプラグインを有効にするだけで、すべてのスクリプトが少し遅れて実行されるようになります。
- JavaScriptベースのlazy loadingを使用していない限り、ペイント指標はおそらく改善されます。
- インラインスクリプトと外部スクリプトの両方で動作します。
デメリット:
- スクリプトが実行されるタイミングを細かく制御できません
- 粗悪なコードで書かれたスクリプトが破損する可能性があります
- JavaScriptを修正するためにJavaScriptを使用することになります
- 実行時間の長いスクリプト(long task)は一切解決しません
方法8:Intersection Observerを使用する
Intersection Observerを使用すると、要素がスクロールされて表示可能なviewportに入ったときに、関数(この場合は外部JavaScriptの読み込み)を実行できます。
<script>
const handleIntersection = (entries, observer) => {
if (entries?.[0].isIntersecting) {
// スクリプトの読み込みや、lazy loading要素のトリガーといった別関数を実行
loadScript('javascript.js');
// observerを解除
observer.unobserve(entries?.[0].target);
}
};
const Observer = new window.IntersectionObserver()
Observer.observe(document.querySelector('footer'));
</script>
これは、間違いなく最も効果的なJavaScript of defer方法です。必要なスクリプトだけを、必要になる直前に読み込みます。しかし残念ながら、実際の開発現場はこれほど単純ではなく、スクロールして表示される要素に紐付けられるスクリプトはそれほど多くありません。
使用すべきケース:
このテクニックを可能な限り活用してください! スクリプトが画面外のコンポーネント(地図、スライダー、フォームなど)としか対話しない場合、これがスクリプトを動的挿入する最適な方法です。
メリット:
- Core Web VitalsのLCPやFCPを妨げません
- 使用されないスクリプトは一切挿入されないため、INPが改善します
デメリット:
- 表示されているviewport内にある可能性のあるコンポーネントには使用すべきではありません
- 基本的な<script src="...">よりもメンテナンスが難しくなります
- レイアウトシフトを引き起こす可能性があります
方法9:readystatechangeを使用する
document.readystatechangeは、'DOMContentLoaded'や'load'イベントの代替として利用できます。'interactive'のreadyStateは、通常、DOM操作やイベントハンドラーの追加を行う必要があるCriticalなスクリプトを実行するのに適したタイミングです。
'complete'のreadyStateは、重要度の低いスクリプトを実行するのに適しています。
document.addEventListener('readystatechange', (event) => {
if (event.target.readyState === 'interactive') {
initLoader();
} else if (event.target.readyState === 'complete') {
initApp();
}
});
方法10:タイムアウトなしのsetTimeoutを使用する
setTimeoutはページ速度(pagespeed)コミュニティで敬遠されがちですが、非常に過小評価されている手法です。誤用されることが多いため悪い評判を得ていますが、多くの開発者はsetTimeoutを設定されたミリ秒数だけスクリプトの実行を遅らせるためだけのものと誤解しています。確かにその通りですが、setTimeoutは実際にはもっと興味深い役割を果たします。ブラウザのイベントループの最後に新しいタスクを作成するのです。この挙動を利用して、タスクを効果的にスケジュールできます。また、long taskをより小さなタスクに分割するのにも役立ちます。
<script>
setTimeout(() => {
// スクリプトを読み込むか、別の関数を実行する
console.log('- 0msのsetTimeoutから呼び出されました')
}, 0);
console.log('- 最後に記述されましたが、最初に実行されました')
/*
出力:
- 最後に記述されましたが、最初に実行されました
- 0msのsetTimeoutから呼び出されました
*/
</script>
使用すべきケース:
setTimeoutはブラウザのイベントループに新しいタスクを作成します。main threadが順次実行される多数の関数呼び出しによってブロックされている場合に使用します。
メリット:
- 実行時間の長いコードをより小さな処理に分割できます。
デメリット:
- setTimeoutはやや大雑把な手法であり、重要なスクリプトに優先順位を付けることはできません。
- 実行予定のコードがイベントループの最後に追加されます。
方法11:タイムアウトを指定したsetTimeoutを使用する
0msを超えるタイムアウトを指定してsetTimeoutを呼び出すと、さらに興味深いことが起こります。
<script>
setTimeout(() => {
// スクリプトを読み込むか、別の関数を実行する
console.log('- 10msのsetTimeoutから呼び出されました')
}, 10);
setTimeout(() => {
// スクリプトを読み込むか、別の関数を実行する
console.log('- 0msのsetTimeoutから呼び出されました')
}, 0);
console.log('- 最後に記述されましたが、最初に実行されました')
/*
出力:
- 最後に記述されましたが、最初に実行されました
- 0msのsetTimeoutから呼び出されました
- 10msのsetTimeoutから呼び出されました
*/
</script>
使用すべきケース:
1つのスクリプトを別のスクリプトの後にスケジュールする簡単な方法が必要な場合、短いタイムアウトを設定するだけで実現できます。
メリット:
- すべてのブラウザでサポートされています
デメリット:
- 高度なスケジュール機能は提供されません
方法12:Promiseを使用してマイクロタスクを設定する
マイクロタスクを作成することも、JavaScriptをスケジュールする興味深い方法です。マイクロタスクは、現在の実行ループが終了した直後に実行されるようにスケジュールされます。
<script>
const myPromise = new Promise((resolve, reject) => {
resolve();
}).then(
() => {
console.log('- Promiseの後にスケジュールされました')
}
);
console.log('- 最後に記述されましたが、最初に実行されました')
/*
出力:
- 最後に記述されましたが、最初に実行されました
- Promiseの後にスケジュールされました
*/
</script>
使用すべきケース:
あるタスクの直後にタスクをスケジュールする必要がある場合に使用します。
メリット:
- タスクの実行完了直後にマイクロタスクがスケジュールされます。
- 同じイベント内の重要度の低いJavaScriptコードの処理を遅らせるために、マイクロタスクを使用できます。
デメリット:
- main threadを小さな処理に分割しません。ブラウザがユーザー入力に応答する機会はありません。
- 何を行っているか正確に理解している場合を除き、Core Web Vitalsを改善するためにマイクロタスクを使用する必要はおそらくありません。
方法13:マイクロタスクを使用する
queueMicrotask()を使用することでも同じ結果を得られます。Promiseと比較した queueMicrotask()のメリットは、動作がわずかに高速であり、Promiseの処理を行う必要がない点です。
<script>
queueMicrotask(() => {
console.log('- マイクロタスクです')
})
console.log('- 最後に記述されましたが、最初に実行されました')
/*
出力:
- 最後に記述されましたが、最初に実行されました
- マイクロタスクです
*/
</script>
方法14:requestIdleCallbackを使用する
window.requestIdleCallback()メソッドは、ブラウザのアイドル期間中に呼び出される関数をキューに登録します。これにより、アニメーションや入力応答などのレイテンシに敏感なイベントに影響を与えることなく、メインのイベントループでバックグラウンドや低優先度の処理を実行できます。関数は原則として先入れ先出しの順序で呼び出されますが、タイムアウトが指定されているコールバックは、タイムアウトが経過する前に実行されるよう、必要に応じて順序を無視して呼び出されることがあります。
<script>
requestIdleCallback(() => {
const script = document.createElement('script');
script.src = 'javascript.js';
document.head.appendChild(script);
});
</script>
使用すべきケース:
Nice to haveなスクリプトや、ユーザー入力後の重要ではないタスクを処理する場合に使用します。
メリット:
- ユーザーへの影響を最小限に抑えてJavaScriptを実行できます
- INPを改善する可能性が極めて高いです
デメリット:
- コードが必ず実行されるという保証はありません
方法15:postTaskを使用する
scheduler.postTask()メソッドを使用すると、タスク実行までの最小遅延時間、タスクの優先度、およびタスクの優先度変更や中断(アボート)に使用できるシグナルをオプションで指定できます。このメソッドはPromiseを返し、タスクのコールバック関数の結果によってresolveされるか、中断理由またはタスク内で発生したエラーによってrejectされます。
<script>
scheduler.postTask(() => {
const script = document.createElement('script');
script.src = 'javascript.js';
document.head.appendChild(script);
}, { priority: 'background' });
</script>
使用すべきケース:
優先度を細かく制御する必要がある場合、postTaskはスクリプトのスケジュール実行に最適なAPIです。
メリット:
- JavaScriptの実行スケジュールの完全な制御が可能です!
デメリット:
- Safariではサポートされていません。Chrome 94以降、Edge 94以降、Firefox 142以降でサポートされています。すべての環境をカバーするには、機能検出とsetTimeoutを使用したfallbackを使用してください。
方法16:scheduler.yield()を使用する
scheduler.yield()は、long taskを分割するための最新の方法です。新しいタスクでresolveされるPromiseを返し、処理の合間にブラウザがユーザー入力に応答できるようにします。setTimeoutとは異なり、再開処理はキューにある他のタスクよりも優先されるため、列の最後尾に追いやられることなく、中断した場所から処理を再開できます。
<script>
async function processItems(items) {
for (const item of items) {
doWork(item);
await scheduler.yield();
}
}
</script>
これは、INPを改善するための最も強力なツールです。main threadを数百ミリ秒間ブロックするlong taskを、yieldポイントで区切られた小さな処理に分割できます。ブラウザは各yieldポイントでユーザー入力を処理できます。このパターンの具体的な実装手順については、main threadへのyield方法を参照してください。
Safariはまだscheduler.yield()をサポートしていないため、必ずfallbackを含めてください:
<script>
function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function processItems(items) {
for (const item of items) {
doWork(item);
await yieldToMain();
}
}
</script>
使用すべきケース:
main threadをブロックする、実行時間の長いJavaScriptがある場合はいつでもこれを使用します。インタラクションハンドラーや、ループ処理でデータを処理するコードのINPを改善するための推奨アプローチです。
メリット:
- キュー内の順序を失うことなくlong taskを分割できます
- 後続処理は他のキューされたタスクよりも前に実行されます(イベントループの最後尾に送られるsetTimeoutとは異なります)
- ブラウザがユーザー入力に応答する機会を作ることで、INPを直接改善します
デメリット:
- Safariではサポートされていません。Chrome 129以降、Edge 129以降、Firefox 142以降でサポートされています。
- すべてのブラウザをカバーするにはfallbackが必要です(setTimeoutがポリフィルとして機能します)
これらのテクニックを適用した後は、Real User Monitoringで改善を検証してください。Lighthouseのスコアは単なる出発点であり、Googleがランキングで使用するのは実際のユーザーからのfield dataです。CoreDashは実際の訪問者からINPとすべてのCore Web Vitalsを追跡するため、適用したdefer戦略が本番環境で実際に機能しているかどうかを確認できます。