JavaScript の読み込みを最初の操作まで遅らせると、その最初の操作は誰が受け取るのでしょうか。
先に要点です。
| 最初の操作(ページを開いてすぐ) | 遅延スクリプトのハンドラ | ページの動き |
|---|---|---|
| チェックボックスのラベルをタップ |
change 届かず |
チェックは入る |
| ボタンをタップ |
click 届かず |
何も起きない |
| 入力欄をタップして1文字入力 |
input 届かず |
文字は入る |
| 送信ボタンをタップ |
submit 届かず |
JS を通らずにフォームが送信され、ページ遷移 |
| 先に別の操作をしてから待った時間 | スクリプト到着まで 0ms | 300ms |
|---|---|---|
| 0ms | 届いた | 届かず |
| 100ms | 届いた | 届かず |
| 300ms | 届いた | 届いた |
| 1000ms | 届いた | 届いた |
4種類とも、最初の1回は落ちました。
2回目からは全部届きます。
境目は、ハンドラが登録されるまでの時間でした。
先に断っておくと、遅延JSが最初の操作を取りこぼすことは知られています。WP Rocket にも、初回のクリックが遅延スクリプトの読み込み後まで待たされる、という報告が issue #7296 に上がっているようです。ここで足すのは、どのイベントが落ちるかを種類ごとに測った結果と、落ちる時間の幅です。
9/25 サイドバーが1回目だけ真っ白
発端は自分のサイトでした。
Cocoon テーマのスマホ用スライドインサイドバーが、1回目のタップだけ空で開きます。
2回目からは正常。
使っているキャッシュプラグインは、自作の Prime Cache です。
JavaScript の遅延(Delay JS)を有効にしていました。
2回目からは直るので、見落としやすい種類の不具合でした
Cocoon のドロワーは、CSS のチェックボックスで開閉します。
中身のサイドバーは、javascript.js がチェックボックスの change を受けて移動させます。
その javascript.js が遅延されていました。
9/25 ローダーが見ているイベント
Prime Cache のローダーは、次のイベントで遅延スクリプトの読み込みを始めます。
var triggerEvents = [
'keydown', 'mousedown', 'mousemove', 'touchstart',
'touchend', 'wheel', 'scroll'
];
click は入っていません。
コメントには、リンクが必ず即座に動くように、クリックは止めない、とあります。
タップすると、順番はこうなります。
-
touchstartで読み込みが始まる -
clickが発火し、チェックボックスが切り替わり、changeが発火する - そのあとで
javascript.jsが実行され、ハンドラが登録される
change の時点で、受け取る人がいません。
ローダーは click も change も再生しないので、それきりです。
読めば分かる話でした。読んだのは、壊れてからでした
9/26 種類ごとに測る
change だけなのか。
ほかのイベントはどうか。
WordPress 抜きで最小のページを作りました。
ローダーは Prime Cache 1.10.62 の pc-delay-loader.js をそのまま使っています(GitHub)。
<script>window.pcDelayTimeout = 0;</script>
<script src="pc-delay-loader.js"></script>
...
<label for="cb" id="lb">メニュー</label><input type="checkbox" id="cb">
<button id="btn">ボタン</button>
<input id="tx">
<form id="fm"><button id="sb">送信</button></form>
<script type="pc-delay/javascript" data-pc-src="handlers.js"></script>
遅延される handlers.js では、jQuery の $(document).on() と同じ委譲の形と、要素への直接登録の両方を置きました。
window.__boundAt = performance.now();
function log(n) { (window.__log = window.__log || []).push(n); }
document.addEventListener('change', e => { if (e.target.id === 'cb') log('change-deleg'); });
document.addEventListener('click', e => { if (e.target.id === 'btn') log('click-deleg'); });
document.addEventListener('input', e => { if (e.target.id === 'tx') log('input-deleg'); });
document.addEventListener('submit', e => { e.preventDefault(); log('submit-deleg'); });
document.getElementById('cb').addEventListener('change', () => log('change-direct'));
document.getElementById('btn').addEventListener('click', () => log('click-direct'));
submit のハンドラは preventDefault() しています。
Ajax で送るフォームの形です。
これを Playwright(Chromium 141)で開いて、最初の操作をしてから __log を読みます。
スマホは Pixel 7 のエミュレーション。
handlers.js は、ローカルのサーバーで 0ms と 300ms の遅れを付けて返しました。
const ctx = await browser.newContext({ ...devices['Pixel 7'] });
const page = await ctx.newPage();
await page.goto(url);
await page.waitForTimeout(500);
await page.tap('#lb'); // 最初の操作
await page.waitForTimeout(1500);
console.log(await page.evaluate(() => window.__log));
9/26 全部落ちた
冒頭の1つめの表が結果です。
change だけではありませんでした。
click も input も submit も、最初の1回は落ちます。
委譲でも直接登録でも同じ。
マウスのクリックでも、Tab で移ってスペースキーでも同じでした。
submit がいちばん怖い
送信ボタンのタップでは、ハンドラがいないので preventDefault() されません。
フォームは素のまま送信され、ページが遷移しました。
Ajax で送るつもりのフォームが、JS の検証も通らずに GET で飛んでいきます。
ハンドラが登録された時刻も測りました。
最初の操作から、スクリプト到着 0ms のときで約100ms、300ms のときで約350〜400ms 後です。
この間に起きたイベントは、全部落ちます。
9/26 先に触っていれば間に合う
では、ページを開いてから少しスクロールした人はどうか。
スクロール(wheel)でローダーを起こしておき、待ってからタップしました。
デスクトップでは、マウスを動かしてからクリックしています。
冒頭の2つめの表がそれです。スマホのスクロールでも、デスクトップのマウス移動でも、結果は同じでした。
スクリプトの到着に 300ms かかる設定だと、100ms 後のタップはまだ落ちます。
300ms 待てば届きます。
デスクトップで取りこぼしにくいのは、クリックの前にたいていマウスが動くからです。
ローダーは最初の mousemove を1回見送りますが、その次で起きます。
スマホにはそれがありません。
ページを開いて最初にメニューを押す人は、そのタップが最初の合図になります。
Prime Cache の Delay JS は、既定ではスマホにだけ掛かります。
取りこぼしやすい側にだけ掛かっている、ということでもあります。
直し方
Cocoon の件は、1.10.62 で直しました。
遅延しない小さなスクリプトを1本だけ置いて、サイドバーを移す処理を肩代わりさせています。
Cocoon のハンドラが動き出したら、そちらに任せます。
一般には、手は3つだと思います。
| 手 | 効くところ | 代わりに失うもの |
|---|---|---|
| そのスクリプトを遅延の対象から外す | 確実 | 遅延の効果が減る |
| 最初の操作を捕まえておき、読み込み後に再生する | 全体 | リンクの即応性。再生の順序の管理 |
| 初回の操作に要る処理だけ、遅延しない小さなスクリプトで持つ | 対象の箇所 | 箇所ごとに手当てが要る |
Prime Cache は、リンクを止めない方針なので、2つめは選んでいません。
1.10.62 は3つめです。
次の自分に渡すメモ
- 遅延JSで落ちるのは
changeだけではない。最初のclickinputsubmitも落ちる -
submitが落ちると、preventDefault()されないままフォームが送信される - 落ちる幅は、最初の操作からハンドラ登録まで。スクリプトの到着時間にほぼ比例する
- デスクトップは直前の
mousemoveで間に合いやすい。スマホは最初のタップが合図そのもの - 遅延させるスクリプトを選ぶときは、ページを開いて最初に押されうるものを先に探す
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/