Aby wiarygodnie zmierzyć tekst w DOM, poczekaj na odpowiedni arkusz stylów, umieść tekst w docelowym układzie, a następnie zaczekaj na document.fonts.ready przed odczytaniem geometrii. Ten poradnik pokazuje tę kolejność w przeglądarce na komputerze Mac i wyjaśnia, kiedy należy ją powtórzyć.

Chodzi tu o zmierzenie syntetycznej etykiety lub akapitu z zawijanym tekstem na stronie przeglądarki, na przykład w celu ustalenia, ile miejsca potrzebuje karta. To przeglądarka powinna układać tekst; Twój kod powinien zadbać o gotowość i odczytać wynikowe wymiary.

Na jaki sygnał gotowości należy poczekać?

Istnieją osobne etapy gotowości dokumentu, ładowania arkusza stylów i układu tekstu z użyciem czcionek. Wybierz etap odpowiedni dla kontrolowanych przez Ciebie działań, a następnie wykonaj pomiar, gdy treść i jej style będą już gotowe.

Sygnał Co potwierdza Na co jeszcze trzeba zwrócić uwagę
DOMContentLoaded Zakończono parsowanie i wykonywanie skryptów odroczonych Nie czeka bezpośrednio na arkusze stylów
Zdarzenie load arkusza stylów Arkusz stylów i jego zaimportowana zawartość zostały załadowane i sparsowane Ładowanie czcionek i wynikowy układ elementu docelowego
Zdarzenie load okna Załadowano początkowe zasoby strony, w tym arkusze stylów, z wyjątkiem zasobów ładowanych leniwie Późniejsze zmiany aplikacji i gotowość czcionek
document.fonts.ready Zakończono ładowanie i układ związane z czcionkami używanymi w dokumencie Późniejsze zmiany treści, stylów lub rozmiaru

MDN opisuje niuans związany z arkuszami stylów: samo zdarzenie DOMContentLoaded na nie nie czeka, choć skrypty odroczone czekają na arkusze stylów i mogą przez to pośrednio opóźnić to zdarzenie. Jest przydatne do wyszukiwania elementów, ale nie stanowi kompletnej gwarancji gotowości do pomiaru. Dokument: zdarzenie DOMContentLoaded.

Jeśli samodzielnie dodajesz arkusz stylów, przed wstawieniem odnośnika przypisz mu procedury obsługi zdarzeń load i error. Według MDN zdarzenie load arkusza stylów występuje po jego załadowaniu i sparsowaniu, tuż przed rozpoczęciem stosowania stylów. Przetwarzanie arkuszy stylów w WHATWG rozróżnia także pomyślne załadowanie od zdarzenia error. Zdarzenia ładowania arkusza stylów, Standard HTML: odnośniki do arkuszy stylów.

W przypadku początkowego ładowania zasobów strony zdarzenie load okna stanowi szerszy punkt kontrolny. Skrypt uruchomiony później musi sprawdzić document.readyState przed zapisaniem się na zdarzenie, ponieważ zdarzenie, które już wystąpiło, nie uruchomi nowo przypisanej procedury obsługi. Kod aplikacji, który później wstawia arkusz stylów, nadal musi jawnie śledzić to wstawienie. Okno: zdarzenie load, Sprawdzanie gotowości dokumentu.

Co tak naprawdę gwarantuje gotowość czcionek?

Specyfikacja W3C CSS Font Loading wyjaśnia, że ready zostaje spełniona po zakończeniu odpowiednich operacji układu, gdy do ich wykonania nie są już potrzebne kolejne czcionki. Obejmuje to sytuacje, w których użycie czcionki zastępczej powoduje dodatkowe ładowanie. Ta sama obietnica zostaje spełniona tylko raz, a późniejsze zmiany mogą spowodować kolejne ładowanie czcionek. Odczytuj document.fonts.ready po zmianach istotnych dla danego pomiaru, zamiast bez końca polegać na jednej obietnicy utworzonej przy starcie. CSS Font Loading Module Level 3: atrybut ready.

Gotowość dotyczy czcionek używanych przez dokument. Nie oznacza, że pobrano każdą zadeklarowaną czcionkę. MDN zaznacza, że font-display: optional może sprawić, iż zestaw używanych czcionek będzie się różnić od zestawu zadeklarowanego. Ukończony pomiar może więc prawidłowo opisywać tekst wyświetlony czcionką zastępczą. Dokument: właściwość fonts.

Zanim napiszesz kod oczekujący, zdecyduj, jaki wynik jest Ci potrzebny:

  • Zmierz aktualnie renderowany wynik: zaczekaj na gotowość czcionek i odczytaj geometrię DOM, akceptując wybór czcionek zastępczych dokonany przez przeglądarkę.
  • Wymagaj zadeklarowanego i możliwego do pobrania kroju: jawnie załaduj pasujący krój, obsłuż błąd, a następnie zaczekaj na gotowość czcionek dokumentu przed pomiarem.
  • Mierz zmieniającą się treść: powtarzaj sekwencję po każdej istotnej aktualizacji i odrzucaj wyniki zastąpione kolejną aktualizacją.

W drugim przypadku document.fonts.load() przyjmuje specyfikację czcionki CSS i opcjonalny tekst próbki. Dla syntetycznej zadeklarowanej rodziny o nazwie SampleFace specyfikacja może mieć postać 400 18px \"SampleFace\", a tekst może brzmieć Panel Alpha. Zwrócona obietnica zostaje odrzucona, jeśli pasujący krój nie załaduje się. Argument tekstowy wybiera kroje na podstawie zadeklarowanych zakresów Unicode; nie weryfikuje pokrycia poszczególnych glifów. FontFaceSet: metoda load().

Nie używaj document.fonts.check() zamiast tego rozwiązania. MDN wyjaśnia, że może ono zwrócić true nawet wtedy, gdy żądana czcionka jest niedostępna. Podobnie W3C określa, że ready nigdy nie zostaje odrzucona, więc samo pomyślne zakończenie nie dowodzi, że ładowanie czcionki się powiodło. FontFaceSet: metoda check(), CSS Font Loading: atrybut ready.

Wykonaj kompletny przykład od załadowania arkusza stylów do pomiaru

Na komputerze Mac otwórz about:blank w aktualnej przeglądarce na komputer, otwórz narzędzia deweloperskie i wybierz kartę Console. Uruchom poniższy kompletny program JavaScript jeden raz. Tworzy on własny neutralny tekst próbki i arkusz stylów, więc nie wymaga serwera, biblioteki ani pliku czcionki.

W przykładzie celowo użyto system-ui. Pokazuje on sekwencję oczekiwania bez zależności od pobieranego zasobu. Na stronie korzystającej z czcionek internetowych zachowaj tę kolejność i pozwól, by zasoby zapewniały rzeczywiste deklaracje czcionek tej strony. Przykład przeglądarkowy został sprawdzony, ale nie został uruchomiony na potrzeby tego tekstu.

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

Najważniejsza jest kolejność: zakończenie ładowania arkusza stylów, przygotowanie układu elementu docelowego, gotowość czcionek, a następnie końcowe odczyty geometrii. Pierwszy odczyt geometrii ustala bieżący układ; jego wartość jest celowo odrzucana. Końcowe odczyty opisują stan po uzyskaniu gotowości czcionek. Limit dziesięciu sekund jest przykładowym ograniczeniem operacyjnym: po jego upływie zgłaszana jest niedokończona próba i nie powstaje żaden pomiar.

Przy dostosowywaniu przykładu sprawdzaj fontSize i lineHeight razem z wymiarami. getComputedStyle() zwraca rozwiązane wartości właściwości po zastosowaniu aktywnych arkuszy stylów, dzięki czemu pomaga wykryć rozbieżność selektora lub kaskady. Jego wartość fontFamily opisuje listę rodzin CSS; nie należy jej traktować jako dowodu, który krój dostarczył każdy glif. Okno: metoda getComputedStyle().

Który pomiar DOM odpowiada na Twoje pytanie?

Użyj element.getBoundingClientRect(), gdy potrzebujesz zewnętrznej przestrzeni zajmowanej przez kartę lub akapit. Zwracany prostokąt obejmuje dopełnienie i obramowanie, a jego pozycja jest określona względem obszaru wyświetlania. W przykładzie zadeklarowana szerokość pudełka obramowania wynosi 280px; dostępna szerokość obszaru treści tekstu to 254px, co obliczono jako 280 − 24 − 2. To obliczenie opisuje podane style CSS, a nie zaobserwowany wynik przeglądarki. Element: metoda getBoundingClientRect().

Użyj obiektu Range obejmującego tekst, gdy potrzebujesz jego fragmentów układu. Range.getClientRects() zwraca prostokąty zakresu, a nie tylko prostokąt obejmujący cały akapit. Zachowaj kolekcję: spłaszczenie zawijanego tekstu do jednej szerokości pozbawia Cię przydatnych informacji o zajmowanych przez niego obszarach. W ogólnej implementacji nie nazywaj długości tablicy lineCount; złożona treść śródliniowa może dawać wiele prostokątów. Range: metoda getClientRects().

Na czas pomiaru pozostaw oryginalny fragment tekstu bez zmian. Dodanie elementu opakowującego każdy znak może wprowadzić granice formatowania wpływające na kształtowanie tekstu. Projekt specyfikacji CSS Text W3C określa warunki, w których granice elementów śródliniowych zachowują lub przerywają kształtowanie. Pomiar istniejącego DOM za pomocą zakresu pozwala uniknąć dodawania takich elementów. CSS Text Module Level 3: kształtowanie tekstu na granicach elementów.

Canvas pełni inną rolę. measureText() zwraca metryki tekstu na canvasie dla podanego ciągu. W tym zadaniu — ustalaniu rzeczywistego zawijania tekstu wewnątrz karty DOM — po uzyskaniu gotowości odczytaj DOM. Pomiar na canvasie traktuj jako osobne obliczenie, z własnymi ustawieniami tekstu. CanvasRenderingContext2D: metoda measureText().

Co zrobić, gdy pomiary nadal się zmieniają?

Zanim dodasz kolejne opóźnienie, wykonaj następujące czynności diagnostyczne:

  1. Potwierdź, że oczekiwane zakończenie ładowania dotyczy arkusza stylów używanego do stylowania elementu docelowego. W przypadku dynamicznie wstawianych arkuszy powiąż obietnice ładowania z kodem, który je wstawia.
  2. Sprawdź rozwiązane ustawienia typografii i szerokość elementu docelowego. Załadowany arkusz stylów nadal może przegrać w kaskadzie.
  3. Upewnij się, że tekst docelowy i jego planowany kontener istnieją przed oczekiwaniem na gotowość czcionek.
  4. Zdecyduj, czy czcionka zastępcza jest dopuszczalna. Jeśli wymagany jest konkretny zadeklarowany krój, załaduj go jawnie i zgłoś błąd w razie niepowodzenia.
  5. Sprawdź, czy po oczekiwaniu kolejna aktualizacja aplikacji nie zmieniła tekstu, klas lub kontenera. Rozpocznij pomiar od nowa dla tego nowego stanu.

Wywołanie zwrotne klatki nie stanowi bariery ładowania zasobów. requestAnimationFrame() planuje wywołanie zwrotne przed ponownym rysowaniem, a przeglądarki często wstrzymują je na kartach działających w tle. Używaj go, gdy planowanie klatek służy Twojej aplikacji, ale nie zastępuj nim obietnic ładowania arkuszy stylów ani czcionek, nawet jeśli planujesz jedno lub dwa wywołania zwrotne klatki. Okno: metoda requestAnimationFrame().

W przypadku zmiennego rozmiaru układu obserwuj odpowiedni kontener za pomocą ResizeObserver i planuj nowe pomiary, gdy zmienią się jego wymiary. Obserwator zgłasza zmiany rozmiaru obszaru treści lub pudełka obramowania; nie jest uniwersalnym powiadomieniem o zmianie tekstu. Uruchamiaj pomiar także w odpowiedzi na operacje aplikacji, które zastępują tekst lub zmieniają typografię. ResizeObserver.

Jeśli korzystasz już z Playwright, wykonaj oczekiwanie na gotowość po stronie przeglądarki i odczyty geometrii wewnątrz asynchronicznego wywołania zwrotnego page.evaluate(). Playwright czeka na zwróconą przez nie obietnicę i przekazuje wynik do środowiska wywołującego. Zwracaj zwykłe liczbowe pola prostokąta, tak jak w przykładzie, i jawnie przekazuj do wywołania zwrotnego wszystkie wartości po stronie wywołującej. Wykonywanie kodu JavaScript.

Przydatny pomiar odnosi się do konkretnego tekstu, stanu arkuszy stylów, wyniku ładowania czcionek i rozmiaru kontenera. Ustal te dane wejściowe, zaczekaj na odpowiednie ładowanie i zmierz niezmieniony DOM. Powtarzaj pomiar, gdy te dane się zmieniają, aby zapisane wymiary nadal odpowiadały rzeczywistemu pytaniu o układ.

Źródła