DOMテキストを確実に計測するには、対象のスタイルシートを待ち、テキストを意図したレイアウトに配置してから、ジオメトリを読み取る前に document.fonts.ready を待機します。このガイドでは、Macのデスクトップブラウザーでの手順と、再実行が必要なタイミングを説明します。
ここでの作業は、ブラウザーのページ上で合成ラベルや折り返された段落を計測することです。たとえば、カードに必要なスペースを判断します。テキストのレイアウトはブラウザーに任せ、コードでは準備完了を確立して、結果のボックスを読み取ります。
どの準備完了シグナルを待つべきですか?
ドキュメントの構築、スタイルシートの読み込み、フォントのレイアウトには、それぞれ別の節目があります。自分が制御する作業に合った節目を選び、コンテンツとスタイルを適用してから計測します。
| シグナル | 確立される状態 | なお確認が必要なこと |
|---|---|---|
DOMContentLoaded |
解析と遅延スクリプトの実行が完了した | スタイルシートを直接待つイベントではありません |
スタイルシートの load イベント |
そのスタイルシートとインポートしたコンテンツの読み込みと解析が完了した | フォントの読み込みと、その結果としての対象要素のレイアウト |
ウィンドウの load イベント |
遅延読み込みリソースを除き、スタイルシートを含む初期ページリソースの読み込みが完了した | その後のアプリケーション変更とフォントの準備状況 |
document.fonts.ready |
ドキュメントで使用されるフォントに関わる読み込みとレイアウトが完了した | その後のコンテンツ、スタイル、サイズの変更 |
MDNはスタイルシートに関する注意点を説明しています。DOMContentLoaded 自体はスタイルシートを待ちませんが、遅延スクリプトはスタイルシートを待つため、その影響でイベントが間接的に遅れることがあります。要素を探す用途には便利ですが、計測の条件としては不完全です。Document: DOMContentLoaded event。
自分で挿入するスタイルシートについては、リンクを挿入する前に、その load と error のハンドラーを登録します。MDNによると、スタイルシートの load イベントは、読み込みと解析の後、スタイルの適用が始まる直前に発生します。WHATWGのスタイルシート処理でも、読み込みの成功と error イベントは区別されています。Stylesheet load events、HTML Standard: Stylesheet links。
ページの初期リソース読み込みでは、ウィンドウの load イベントがより広い節目になります。その後に開始するスクリプトは、購読する前に document.readyState を確認する必要があります。すでに発生したイベントは、新たに登録したハンドラーを実行しないためです。後からスタイルシートを挿入するアプリケーションコードでは、その挿入を引き続き明示的に追跡する必要があります。Window: load event、Checking document readiness。
フォントの準備完了が実際に保証すること
W3CのCSS Font Loading仕様によると、ready は、関連するレイアウト処理が完了し、その処理に必要な追加フォント読み込みがなくなった時点で解決します。これには、フォントのフォールバックによって追加の読み込みが発生する場合も含まれます。この同じプロミスが解決するのは一度だけで、その後の変更によってさらにフォントが読み込まれることがあります。起動時のプロミスをずっと使い続けるのではなく、各計測に関係する変更を加えた後に document.fonts.ready を読み取ります。CSS Font Loading Module Level 3: The ready attribute。
準備完了の対象は、ドキュメントで使用されるフォントです。宣言されたすべてのフォントがダウンロードされたという意味ではありません。MDNは、font-display: optional によって、使用されるフォントの集合が宣言されたフォントの集合と異なる場合があると明記しています。そのため、計測が完了しても、フォールバックフォントで描画されたテキストを正しく表していることがあります。Document: fonts property。
待機コードを書く前に、必要な結果を決めてください。
- 現在の描画結果を計測する: フォントの準備完了を待ち、ブラウザーのフォールバック判断を受け入れてDOMのジオメトリを読み取ります。
- 宣言されたダウンロード可能なフォントフェイスを必須とする: 対応するフェイスを明示的に読み込み、失敗を処理してから、ドキュメントのフォント準備完了を待って計測します。
- 変化するコンテンツを計測する: 関連する更新のたびに手順を繰り返し、別の更新によって古くなった結果は破棄します。
2番目の結果が必要な場合、document.fonts.load() はCSSのフォント指定と任意のサンプルテキストを受け取ります。SampleFace という合成ファミリーを宣言している場合、指定は 400 18px \"SampleFace\"、テキストは Panel Alpha となります。一致したフェイスの読み込みに失敗すると、返されたプロミスは拒否されます。テキスト引数は、宣言されたUnicode範囲を通じてフェイスを選択します。個々のグリフが対応しているかどうかを検証するものではありません。FontFaceSet: load() method。
その要件を document.fonts.check() で代用しないでください。MDNの説明によると、要求したフォントが利用できない場合でも true が返されることがあります。同様に、W3Cの仕様では ready は拒否されないため、正常に完了しただけではフォント読み込みの成功を証明できません。FontFaceSet: check() method、CSS Font Loading: The ready attribute。
スタイルシートから計測までの手順を実行する
Macで現在のデスクトップブラウザーから about:blank を開き、開発者ツールを開いて Console を選択します。次のJavaScriptプログラム全体を一度実行します。プログラムは中立的なサンプルテキストとスタイルシートを自分で作成するため、サーバー、ライブラリー、フォントファイルは必要ありません。
この例では意図的に system-ui を使用しています。ダウンロード可能なアセットに依存せず、待機手順を示すためです。Webフォントを使うページでは、この手順をそのまま使い、そのページに実際に宣言されたフォントからリソースを読み込ませます。この原稿用のブラウザー例は確認済みですが、実行はしていません。
(async () => {
if (!document.fonts || !document.body) {
throw new Error("A document body and CSS Font Loading support are required.");
}
if (document.getElementById("text-measure-demo")) {
throw new Error("Reload the blank tab before running this example again.");
}
async function withDeadline(promise, label, milliseconds = 10000) {
let timer;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error(`${label} did not finish within the deadline.`));
}, milliseconds);
})
]);
} finally {
clearTimeout(timer);
}
}
const paragraph = document.createElement("p");
paragraph.id = "text-measure-demo";
paragraph.textContent =
"Panel Alpha contains a compact label. Panel Beta contains another label.";
document.body.append(paragraph);
const css = `
#text-measure-demo {
box-sizing: border-box;
width: 280px;
margin: 24px;
padding: 12px;
border: 1px solid #777;
font: 400 18px/1.5 system-ui, sans-serif;
letter-spacing: normal;
white-space: normal;
overflow-wrap: normal;
}
`;
const stylesheetURL = URL.createObjectURL(
new Blob([css], { type: "text/css" })
);
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = stylesheetURL;
const stylesheetLoaded = new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = () => reject(new Error("The sample stylesheet failed."));
});
try {
document.head.append(link);
await withDeadline(stylesheetLoaded, "Stylesheet loading");
// Establish the target's styled layout before reading font readiness.
paragraph.getBoundingClientRect();
await withDeadline(document.fonts.ready, "Font readiness");
const style = getComputedStyle(paragraph);
const range = document.createRange();
range.selectNodeContents(paragraph);
const copyRect = rect => ({
x: rect.x, y: rect.y, width: rect.width, height: rect.height
});
console.log({
text: paragraph.textContent,
fontFamily: style.fontFamily,
fontSize: style.fontSize,
lineHeight: style.lineHeight,
elementBox: copyRect(paragraph.getBoundingClientRect()),
textFragments: Array.from(range.getClientRects(), copyRect)
});
} catch (error) {
link.remove();
paragraph.remove();
throw error;
} finally {
link.onload = null;
link.onerror = null;
URL.revokeObjectURL(stylesheetURL);
}
})().catch(error => console.error(error));
重要な順序は、スタイルシートの読み込み完了、対象のレイアウト確立、フォントの準備完了、最後にジオメトリの読み取りです。最初のジオメトリ読み取りで現在のレイアウトを確立しますが、その値は意図的に破棄します。最後の読み取りでは、フォントの準備完了後の結果を取得します。10秒の期限は運用上の上限の例です。期限切れの場合は未完了として報告し、計測結果は返しません。
この例を応用する際は、ボックスとともに fontSize と lineHeight を確認します。getComputedStyle() は、適用中のスタイルシートが反映された後の解決済みプロパティ値を返すため、セレクターやカスケードの不一致を検出するのに役立ちます。その fontFamily 値はCSSのフォントファミリーリストを示します。すべてのグリフをどのフェイスが描画したかの証明として扱わないでください。Window: getComputedStyle() method。
どのDOM計測が知りたいことに答えますか?
カードや段落が占める外側のスペースを知りたい場合は、element.getBoundingClientRect() を使います。矩形にはパディングとボーダーが含まれ、位置はビューポートを基準とします。この例では、宣言されたボーダーボックスの幅は 280px、テキストに使えるコンテンツ幅は 254px で、280 − 24 − 2 と計算できます。この計算は指定されたCSSを表すもので、ブラウザーで観測した結果ではありません。Element: getBoundingClientRect() method。
テキストのレイアウト断片を取得するには、テキストに対する Range を使います。Range.getClientRects() は、段落を囲むボックスだけではなく、範囲の矩形を返します。コレクションを保持してください。折り返されたテキストを1つの幅にまとめると、占有領域に関する有用な情報が失われます。汎用的な実装で配列の長さに lineCount という名前を付けないでください。複雑なインラインコンテンツでは、複数の矩形が生じることがあります。Range: getClientRects() method。
計測中は、元のテキストランをそのまま保ちます。文字ごとにラッパーを追加すると、シェーピングに影響する書式境界が生じることがあります。W3CのCSS Text草案は、インライン境界を越えてシェーピングが保持される条件と分断される条件を定めています。既存のDOMを範囲で計測すれば、こうしたラッパーを追加せずに済みます。CSS Text Module Level 3: Shaping Across Element Boundaries。
Canvasの役割は異なります。measureText() は、指定された文字列に対するCanvasのテキスト計測値を返します。この作業では、DOMカード内のテキストが実際にどう折り返されるかを調べるため、準備完了後にDOMを読み取ります。Canvasでの計測は、独自のテキスト設定を使う別個の計算として扱ってください。CanvasRenderingContext2D: measureText() method。
計測結果が変わり続ける場合はどうすればよいですか?
さらに待ち時間を追加する前に、次の順序で診断します。
- 待ったスタイルシートの読み込み完了が、対象をスタイルするスタイルシートのものか確認します。動的に挿入したスタイルシートでは、挿入コードとともに読み込みプロミスを保持します。
- 対象の解決済みタイポグラフィと幅を確認します。スタイルシートが読み込まれていても、カスケードで負けることがあります。
- フォントの準備完了を待つ前に、対象のテキストと意図したコンテナーが存在することを確認します。
- フォールバックを許容するか判断します。特定の宣言済みフェイスが必須なら、明示的に読み込み、失敗を報告します。
- 待機後に別のアプリケーション更新によって、テキスト、クラス、コンテナーが変更されていないか確認します。状態が変わった場合は、新しい状態について計測をやり直します。
フレームコールバックはリソース読み込みの完了を保証するものではありません。requestAnimationFrame() は再描画の前にコールバックを予約しますが、ブラウザーはバックグラウンドタブで通常これを一時停止します。アプリケーションでフレームのスケジューリングが適切な場合に使い、スタイルシートやフォントのプロミスを1回または2回のフレームコールバックで置き換えないでください。Window: requestAnimationFrame() method。
サイズが変わるレイアウトでは、関連するコンテナーを ResizeObserver で監視し、サイズが変わったときに新たな計測を予約します。このオブザーバーはコンテンツボックスまたはボーダーボックスのサイズ変更を通知しますが、あらゆるテキスト変更を知らせるものではありません。テキストの置換やタイポグラフィの変更を行うアプリケーション処理からも、計測を実行してください。ResizeObserver。
すでにPlaywrightを使っている場合は、非同期の page.evaluate() コールバック内で、ブラウザー側の準備完了待ちとジオメトリの読み取りを行います。Playwrightは返されたプロミスを待機し、その結果を呼び出し元の環境に戻します。この例のように、矩形のフィールドは単純な数値として返し、呼び出し側の値はコールバックに明示的に渡します。Evaluating JavaScript。
有用な計測結果は、特定のテキスト、スタイルシートの状態、フォントの結果、コンテナーのサイズに対応しています。これらの入力条件を確立し、関連する読み込み処理を待ってから、手を加えていないDOMを計測します。入力条件が変わったら計測を繰り返し、記録したジオメトリが、実際に知りたいレイアウトの問いに答え続けるようにします。
出典
- CSS Font Loading Module Level 3 — W3C。
- HTML Standard: Link type stylesheet — WHATWG。
- Document: DOMContentLoaded event — MDN Web Docs。
<link>: The HTML External Resource Link element — MDN Web Docs。- Window: load event — MDN Web Docs。
- Document: fonts property — MDN Web Docs。
- FontFaceSet: load() method — MDN Web Docs。
- FontFaceSet: check() method — MDN Web Docs。
- Window: getComputedStyle() method — MDN Web Docs。
- Element: getBoundingClientRect() method — MDN Web Docs。
- Range: getClientRects() method — MDN Web Docs。
- CSS Text Module Level 3 — W3C。
- CanvasRenderingContext2D: measureText() method — MDN Web Docs。
- Window: requestAnimationFrame() method — MDN Web Docs。
- ResizeObserver — MDN Web Docs。
- Evaluating JavaScript — Playwright。