LighthouseのForced reflow:意味と修正方法
スクリプトがページを変更した直後にサイズを要求すると、ブラウザは即座にレイアウトを再計算します。タップ操作が固まるケースと、無害なケースを解説します。
Lighthouseの「Forced reflow」とはどういう意味ですか?
Forced reflow(強制リフロー)とは、スクリプトがページを変更した直後にサイズを要求したため、ブラウザが即座にレイアウトを再計算することです。Lighthouseは該当するスクリプトをForced reflowインサイト(「A forced reflow occurs when JavaScript queries geometric properties」)に一覧表示します。1回なら正常ですが、ループ処理内で発生するとタップ操作が固まります。スクリプトの処理をCSSに任せるか、変更を加える前にすべてのサイズをまとめて読み取ってください。
Forced reflowの項目には何が表示されますか?
Lighthouse 13とPageSpeed Insightsでは、次のように説明されています。「DOMの状態変更によってスタイルが無効化された後、JavaScriptが幾何学的プロパティ(offsetWidthなど)を照会すると、強制リフローが発生します。これによりパフォーマンスが低下する可能性があります。強制リフローとその軽減策についての詳細。」ページの読み込み中に、スクリプトが一度に30 ms以上のレイアウト処理を強制すると、この項目が赤色で表示されます。
以下のレポートは、WordPressのバケーションレンタルサイトを模して作成したコテージ一覧ページのものです。テーマのスクリプトで各説明文を3行に切り詰め、全カードの高さを揃えています。

モバイル版Lighthouse 13.5、DevToolsのスロットリング(Slow 4G、4x CPU)。
- Top function call: レイアウト処理を開始した外側の関数です。短縮表示されたファイルパスにカーソルを合わせるとフルパスが表示され、対象のテーマ、プラグイン、アプリ名を確認できます。別ドメインのファイルは、セッションレコーダーなどのサードパーティタグであることが大半です(変更できないスクリプト)。
- Source: サイズを要求したコードの各行です。
- Total reflow time: 該当の関数やコード行が引き起こしたレイアウト処理の所要時間です。
- Unscored: このインサイト自体はPerformanceスコアには影響しません。ただし、その背後にあるlong task(ページが応答できなくなる50 ms超の処理)が、Total Blocking Timeを通じてスコアを低下させます。
最上段の行(388 ms)がSource行の合計(743 ms)より小さいのは、読み込み中にコテージのスクリプトが2回実行され、最上段には実行時間が長かった方のみが表示されているためです。
スクリプトがブラウザにレイアウトの再計算を強制する理由は?
前述のコテージ一覧ページでは、「All」フィルターをタップしてから48枚のカードが表示されるまでに、ページが約0.3秒間フリーズします。
レイアウトとは、各ボックスの配置場所と大きさをブラウザが計算する処理ステップです。通常はスクリプトの実行完了後に、1フレームにつき1回だけ行われます。しかし、スクリプトがページを変更(テキスト、クラス、高さなど)した直後にoffsetHeight、getBoundingClientRect()、scrollTop、getComputedStyle()などのサイズ情報を読み取ると、ブラウザはその場で値を返すために、スクリプトの実行中に即座にレイアウトを処理せざるを得なくなります。再計算を強制する読み取り操作の網羅的なリストは、Paul Irish氏による一覧で確認できます。
単発のForced reflowであれば、ブラウザが少し後に実行するはずだった処理が前倒しされるだけであることが大半です。しかし、「変更して再読み取り」をループ内で行うと、ループのたびに完全なレイアウト処理が実行されます。これがいわゆるlayout thrashing(レイアウトスラッシング)です。このコテージテーマでは、次のように各説明文を切り詰めていました。
while (el.offsetHeight > max && words.length > 1) {
words.pop();
el.textContent = words.join(' ') + '…';
} 単語を1つ削るたびにテキストが変更されるため、次のoffsetHeightの読み取り時に新たなレイアウト処理が強制されます。これが全カードで、単語ごとに発生します。
Forced reflowによってページはどのくらい遅くなるのか?
テーマのスクリプトを使用した状態(Before)と、CSSで行数制限とカードの高さ揃えを行った状態(After)で、コテージ一覧ページを計測しました。見た目はどちらも同じです。
| Before | After | |
|---|---|---|
| Lighthouseスコア | 84 | 92 |
| Total Blocking Time | 360 ms | 0 ms |
| Total reflow time | 743 ms | なし |
| 「All」のタップ | 320 ms | 16 ms |

Before

After
モバイル版Lighthouse 13.5、DevToolsのスロットリング。
スコアが8ポイント向上した理由はTotal Blocking Timeにあります。強制レイアウトによってページのブロック時間が長くなるためです。タップの遅延は訪問者が直接体感する部分であり、タップやクリックに対する応答速度を測るCore Web Vitalsの指標であるINP(Interaction to Next Paint)に影響します。320 msという数値は良好なINPの基準である200 msを超えており、そのうち298 msはスクリプトが強制したレイアウト処理でした。一方、16 msはわずか1フレーム分です。Lighthouseはタップ操作を行わないため、ChromeでCPUをスマートフォン相当の速度に制限してタップの所要時間を計測しました。
Forced reflowが無害なケースとは?
- 読み込み時にわずかな行が1つだけある場合。30 msを少し超える程度の行は、スクリプトがサイズを要求したためにページの初回レイアウトが少し前倒しで実行されただけ、というケースがよくあります。高さを先にすべて読み取るように書き直したコテージ一覧のテーマスクリプトでも、Lighthouseの4回の試行中1回で37 msの行が1つ表示されましたが、「All」のタップは16 msで済みました。
- 「[unattributed]」という行。コードの行番号が特定されていない状態です。Lighthouseを再実行し、表示が消えるようであれば放置して構いません。
- アニメーションを開始するスクリプト。Bootstrapのモーダルや一部のポップアッププラグインは、CSSトランジションを確実に開始させる目的で意図的にサイズを読み取っています。

モバイル版Lighthouse 13.5、DevToolsのスロットリング。
修正が必要になるのは、Total reflow timeが数百ミリ秒に達している場合、1つのファイルに多数の行が記録されている場合、またはフィルターやメニュー、スライダーのようにタップ時にも実行されるスクリプトである場合です。
タップ時にForced reflowを引き起こしているスクリプトはどれか?
Lighthouseはページの読み込みしか行わないため、Performanceパネルで反応が遅いタップ操作を記録します。
- DevTools(F12)を開いて「Performance」に移動し、CPUを「4x slowdown」に設定してノートPCの挙動をスマートフォンに近づけます。
- 「Record」をクリックし、動作が遅い箇所をタップしてから記録を停止します。
- Insightsサイドバーの「Forced reflow」を開きます。「Call frames that trigger reflow」に関数名とコード行が表示されます。

コテージ一覧で「All」を1回タップした結果: cabins.jsのclampText。
コンソールのログレベルで「Verbose」を有効にすると、同様の内容が「[Violation] Forced reflow while executing JavaScript took 38ms」のように出力されます。
Lighthouseにjquery.min.jsとしか表示されない場合は、テーマやプラグインの代わりにjQueryが読み取りを行っています。Performanceパネルで再読み込みを記録し、赤い三角マークが付いた「Layout」ブロックをクリックしてください。Summaryの「Function stack」にあるjQueryの行の間に、/wp-content/plugins/<name>/のようなパスを持つファイルがあれば、それが原因のプラグインです。
field dataには、実際の訪問者によるタップやクリックのデータが蓄積されています。CoreDashで「INP」の「Breakdown」を開き、「LOAF」でグループ化してください。Chromeは遅延が発生したフレーム(Long Animation Frames)ごとに実行されたスクリプトを記録するため、各行にスクリプトとそれが実行されたインタラクションのINPが表示されます。クリックやタップのハンドラー内で発生したForced reflowは「processing」(各バーの紫色の部分)としてカウントされるため、紫色の部分が長い低速スクリプトから優先的に着手してください。

CoreDashでのスクリプト別INP(デスクトップ、p75): 緑はinput delay、紫はprocessing、青はpresentation。
Forced reflowを解消するには?
まずは「Top function call」に表示されたファイルを確認し、そのスクリプトの処理をCSSで代替できないか検討してください。3行でのテキスト切り詰め(line-clamp)、カードの高さを揃える処理(grid)、固定ヘッダー(position: sticky)、アスペクト比を維持する画像ボックス(aspect-ratio)には、いずれもスクリプトは不要です。スクリプトがなければ、レイアウトが強制されることもありません。
後述の対策を実施した後は、再度同じタップ操作を記録してください。Insightsサイドバーから「Forced reflow」が消えているはずです。続いてLighthouseおよびfield dataで確認してください。なお、Search Consoleのデータ反映には最大28日かかります。
WordPressのテーマ、プラグイン、ページビルダー
Top function callに表示されるパスを見れば、テーマ(/wp-content/themes/)かプラグイン(/wp-content/plugins/)かが分かります。まずは管理画面の設定を確認してください。Elementorの「セクションを伸縮(Stretch Section)」は読み込み時やリサイズ時にJavaScriptでセクション幅を拡張しますが、全幅コンテナを使用したElementor全幅ページレイアウトであればCSSだけで同じ処理を実現できます。Swiperスライダーでスライド表示数を「auto」に設定すると全スライドのサイズが計測されますが、数値を固定すればサイズを読み取ることなく幅が計算されます。
WP RocketなどのプラグインでJavaScriptの実行を遅延させれば警告自体は消えますが、訪問者の最初のタップやスクロール時にスクリプトが動き、結局同じ強制レイアウトが発生します。警告を隠すのではなくスクリプト自体を修正するか、プラグインの開発元に報告し、改善されない場合はプラグインを乗り換えてください。
テーマがJavaScriptでテキストを切り詰めている場合は、子テーマのCSSを使い、該当テーマのクラスに対して次のように指定します。
.cabin .desc {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
} その後、テーマの設定オプションまたはハンドル名を指定したwp_dequeue_script()を使って、テーマ側の切り詰めスクリプトを停止してください。
Shopifyのテーマとアプリ
テーマエディタの「アプリ埋め込み」でアプリを1つずつ無効化し、その都度Lighthouseを再実行してください。警告が消えたら、該当アプリの開発元に問題を報告するか、別のアプリへの乗り換えを検討します。Top function callのパスに/assets/が含まれている場合はテーマのファイルであり、テーマ自体のコードか、アプリによって追加されたコードのいずれかです。Dawnやそれをベースにしたテーマは、大半がサイズの読み取りを1回で済ませて変更を一括処理するため、まずはアプリ側から疑ってください。
独自のJavaScriptとReact
スクリプト側でサイズ計測が不可避な場合は、先にすべてのサイズを読み取り、その後にまとめてDOM変更を行ってください。そうすれば、1回のレイアウト処理ですべての読み取りに対応できます。以下は、各パネルの高さを個別に設定するアコーディオンの例です。
const panels = [...document.querySelectorAll('.panel')];
// パネルごとにレイアウトが発生: 読み取り、書き込み、読み取り、書き込み
panels.forEach(p => { p.style.maxHeight = p.scrollHeight + 'px'; });
// レイアウトは1回のみ: すべての高さを読み取ってから、まとめて書き込み
const heights = panels.map(p => p.scrollHeight);
panels.forEach((p, i) => { p.style.maxHeight = heights[i] + 'px'; }); - リサイズとスクロール: ResizeObserverとIntersectionObserverを使えば、ブラウザが直前に完了したレイアウトからサイズを取得できます。リサイズやスクロールのハンドラー内で直接サイズを読み取るのではなく、これらを利用してください。
- React:
useLayoutEffect内で1度だけ計測し、stateの更新も1度にとどめます。メニュー、ポップオーバー、ツールチップなどは開いたタイミングでマウントし、閉じている要素が読み込み時にサイズ計測を行わないようにしてください。
変更できないスクリプト
セッションレコーダーやヒートマップのタグは、意図的にサイズを読み取っています。たとえばMicrosoft Clarityは、起動時に10箇所以上ものページサイズを読み取り、訪問者がクリックしたすべての要素の位置を取得します。不要なタグを削除するか、一部のアクセスのみをサンプリング記録するか、あるいは調査対象のページにのみ読み込ませるようにしてください。
代替できないプラグインやアプリでForced reflowが発生している、あるいは手元の端末では問題ないのに訪問者のスマートフォンでタップが固まっていませんか?Core Web Vitals power hourでは、最も遅延しているタップの原因スクリプトを特定し、修正、代替、削除のいずれが最適かを一緒に判断します。