Чтобы надежно измерить текст DOM, дождитесь загрузки соответствующей таблицы стилей, поместите текст в его предполагаемое расположение, затем дождитесь document.fonts.ready перед чтением геометрии. Это руководство показывает последовательность в настольном браузере Mac и объясняет, когда её повторять.

Задача здесь — измерить синтетическую метку или переносимый абзац на странице браузера — например, решить, сколько места нужно карточке. Браузер должен выполнить компоновку текста; ваш код должен обеспечить готовность и прочитать получившиеся блоки.

Какой сигнал готовности нужно ожидать?

Существуют отдельные этапы для построения документа, загрузки таблицы стилей и компоновки шрифтов. Выберите этап, соответствующий выполняемой вами работе, затем измеряйте после того, как контент и его стилизация будут на месте.

Сигнал Что он устанавливает Что всё ещё требует внимания
DOMContentLoaded Разбор и выполнение отложенных скриптов завершены Не ожидает напрямую таблицы стилей
Событие load таблицы стилей Таблица стилей и её импортируемый контент загружены и разобраны Загрузка шрифтов и итоговая компоновка цели
Событие load окна Начальные ресурсы страницы, включая таблицы стилей, загружены, кроме ленивых ресурсов Поздние изменения приложения и готовность шрифтов
document.fonts.ready Загрузка и компоновка с использованием шрифтов документа завершены Последующие изменения контента, стилей или размеров

MDN документирует нюанс, связанный с таблицами стилей: DOMContentLoaded сам по себе не ждёт таблиц стилей, хотя отложенные скрипты ждут их и могут поэтому косвенно задерживать событие. Оно полезно для поиска элементов, но является неполным контрактом измерения. Document: DOMContentLoaded event.

Для таблицы стилей, которую вы вставляете сами, присоедините её обработчики load и error перед вставкой link. 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, принимая решения браузера о резервных шрифтах.
  • Требуется конкретный загружаемый набор: явно загрузите соответствующий набор, обработайте неудачу, затем дождитесь готовности шрифтов документа перед измерением.
  • Измерить меняющийся контент: повторяйте последовательность после каждого релевантного обновления и отбрасывайте результаты, замещённые другим обновлением.

Для второго результата 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. Он демонстрирует последовательность ожидания без зависимости от загружаемого актива. На странице, использующей веб-шрифты, сохраняйте последовательность и позвольте объявлениям шрифтов этой страницы поставлять ресурсы. Пример браузера был инспектирован, но не выполнялся для этой рукописи.

(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));

Важный порядок: завершение таблицы стилей, установленная целевая компоновка, готовность шрифтов, затем финальные чтения геометрии. Первое чтение геометрии устанавливает текущую компоновку; его значение намеренно отбрасывается. Финальные чтения описывают результат после готовности шрифтов. Десятисекундный дедлайн — иллюстративный операционный предел: истечение сообщает о незавершённой попытке и не даёт измерения.

Инспектируйте fontSize и lineHeight вместе с блоками при адаптации примера. getComputedStyle() возвращает вычисленные значения свойств после применения активных таблиц стилей, что полезно для обнаружения несоответствия селектора или каскада. Его значение fontFamily описывает CSS-список семейств; его не следует считать доказательством того, какой набор предоставил каждый глиф. Window: getComputedStyle() method.

Какое измерение DOM отвечает на ваш вопрос?

Используйте element.getBoundingClientRect(), когда вам нужно внешнее пространство, занимаемое карточкой или абзацем. Его прямоугольник включает отступы и границы, а его позиция относится к окну просмотра. В образце объявленная ширина border-box — 280px; доступная ширина контента текста — 254px, рассчитанная как 280 − 24 − 2. Это вычисление описывает предоставленный CSS, а не наблюдаемый браузером результат. Element: getBoundingClientRect() method.

Используйте Range над текстом, когда вам нужны фрагменты его компоновки. Range.getClientRects() возвращает прямоугольники для диапазона, а не только обрамляющий блок абзаца. Сохраняйте коллекцию: сведение переносимого текста к одной ширине теряет полезную информацию об занимаемых регионах. Не называйте длину массива lineCount в общей реализации; сложный встроенный контент может давать несколько прямоугольников. Range: getClientRects() method.

Сохраняйте исходный запуск текста нетронутым при измерении. Добавление обёртки вокруг каждого символа может ввести границы форматирования, влияющие на формирование. Черновик CSS Text W3C специфицирует условия, при которых встроенные границы сохраняют или нарушают формирование. Измерение существующего DOM с диапазоном избегает введения этих обёрток. CSS Text Module Level 3: Shaping Across Element Boundaries.

Canvas играет другую роль. measureText() возвращает метрики текста canvas для предоставленной строки. Для этой задачи — поиска фактического переноса текста внутри DOM-карточки — читайте DOM после готовности. Рассматривайте измерение canvas как отдельный расчёт со своими настройками текста. CanvasRenderingContext2D: measureText() method.

Что делать, если измерения всё ещё меняются?

Используйте эту диагностическую последовательность перед добавлением ещё одной задержки:

  1. Подтвердите, что ожидаемое завершение таблицы стилей принадлежит таблице стилей, стилизующей цель. Для динамически вставляемых листов держите их обещания загрузки с кодом вставки.
  2. Инспектируйте вычисленную типографику и ширину цели. Загруженная таблица стилей может всё ещё проигрывать в каскаде.
  3. Подтвердите, что целевой текст и его предполагаемый контейнер присутствуют перед ожиданием готовности шрифтов.
  4. Решите, приемлем ли резервный шрифт. Если требуется конкретный объявленный набор, используйте явную загрузку и сообщайте о неудаче.
  5. Проверьте, не изменило ли другое обновление приложения текст, классы или контейнер после вашего ожидания. Перезапустите измерение для этого нового состояния.

Колбэк кадра не является барьером загрузки ресурсов. requestAnimationFrame() планирует колбэк перед перерисовкой, и браузеры обычно приостанавливают его в фоновых вкладках. Используйте его, когда планирование кадров служит вашему приложению, но не заменяйте обещания таблиц стилей или шрифтов одним или двумя колбэками кадров. Window: requestAnimationFrame() method.

Для изменяемой компоновки наблюдайте за релевантным контейнером с ResizeObserver и планируйте свежие измерения при изменении его размеров. Наблюдатель сообщает об изменениях размеров content-box или border-box; это не универсальное уведомление об изменении текста. Также запускайте измерение от операций приложения, заменяющих текст или меняющих типографику. ResizeObserver.

Если вы уже используете Playwright, выполните ожидание готовности на стороне браузера и чтение геометрии внутри асинхронного колбэка page.evaluate(). Playwright ждёт возвращаемого обещания и возвращает результат в вызывающую среду. Возвращайте простые числовые поля прямоугольника, как в примере, и явно передавайте любые значения вызывающей стороны в колбэк. Evaluating JavaScript.

Полезное измерение принадлежит конкретному тексту, состоянию таблицы стилей, результату шрифта и размеру контейнера. Установите эти входы, дождитесь соответствующей работы загрузки и измерьте нетронутый DOM. Повторяйте, когда эти входы меняются, чтобы записанная геометрия продолжала отвечать на вопрос компоновки, который у вас действительно есть.

Источники