Om DOM-tekst betrouwbaar te meten, wacht je op het relevante stijlblad, plaats je de tekst in de bedoelde lay-out en wacht je vervolgens op document.fonts.ready voordat je de geometrie uitleest. Deze gids laat de volgorde zien in een desktopbrowser op een Mac en legt uit wanneer je die moet herhalen.

De taak hier is het meten van een synthetisch label of een alinea met tekstterugloop op een webpagina — bijvoorbeeld om te bepalen hoeveel ruimte een kaart nodig heeft. De browser moet de tekstopmaak verzorgen; je code moet vaststellen dat alles klaar is en de resulterende kaders uitlezen.

Op welk gereedheidssignaal moet je wachten?

Er zijn afzonderlijke mijlpalen voor het opbouwen van het document, het laden van stijlbladen en de opmaak van lettertypen. Kies de mijlpaal die past bij het werk waarover je controle hebt en meet pas nadat de inhoud en de bijbehorende stijlen zijn ingesteld.

Signaal Wat het bevestigt Waar je nog op moet letten
DOMContentLoaded Het parsen en uitvoeren van uitgestelde scripts is voltooid Er wordt niet rechtstreeks op stijlbladen gewacht
De load-gebeurtenis van een stijlblad Dat stijlblad en de geïmporteerde inhoud ervan zijn geladen en geparseerd Het laden van lettertypen en de resulterende lay-out van het doel
De load-gebeurtenis van het venster De aanvankelijke paginabronnen, waaronder stijlbladen, zijn geladen, met uitzondering van lazy resources Latere wijzigingen door de toepassing en gereedheid van lettertypen
document.fonts.ready Het laden en de opmaak met de door het document gebruikte lettertypen zijn voltooid Latere wijzigingen in inhoud, stijl of afmetingen

MDN beschrijft de nuance rond stijlbladen: DOMContentLoaded wacht zelf niet op stijlbladen, al wachten uitgestelde scripts daar wel op en kunnen ze de gebeurtenis daardoor indirect vertragen. Dit signaal is handig om elementen te vinden, maar vormt geen volledige afspraak over wanneer je kunt meten. Document: DOMContentLoaded event.

Als je zelf een stijlblad toevoegt, koppel dan de handlers voor load en error voordat je de link invoegt. Volgens MDN vindt de load-gebeurtenis van het stijlblad plaats na het laden en parsen, onmiddellijk voordat de stijlen worden toegepast. De verwerking van stijlbladen in WHATWG maakt ook onderscheid tussen succesvol laden en een error-gebeurtenis. Stylesheet load events, HTML Standard: Stylesheet links.

Voor het laden van de aanvankelijke paginabronnen is de load-gebeurtenis van het venster een breder controlepunt. Een script dat daarna start, moet eerst document.readyState controleren voordat het zich abonneert, omdat een gebeurtenis die al heeft plaatsgevonden geen nieuw gekoppelde handler uitvoert. Als toepassingscode later een stijlblad invoegt, moet die invoeging nog steeds expliciet worden gevolgd. Window: load event, Checking document readiness.

Wat garandeert gereedheid van lettertypen precies?

De W3C-specificatie CSS Font Loading legt uit dat ready wordt ingelost nadat de relevante lay-outbewerkingen zijn voltooid en voor dat werk geen lettertypen meer hoeven te worden geladen. Dit geldt ook voor situaties waarin terugval op een ander lettertype tot extra laadwerk leidt. Dezelfde promise wordt maar één keer ingelost; latere wijzigingen kunnen opnieuw leiden tot het laden van lettertypen. Lees document.fonts.ready nadat de wijzigingen die relevant zijn voor elke meting zijn doorgevoerd, in plaats van één startup-promise blijvend te gebruiken. CSS Font Loading Module Level 3: The ready attribute.

Gereedheid heeft betrekking op de lettertypen die het document gebruikt. Dit betekent niet dat elk gedeclareerd lettertype is gedownload. MDN merkt specifiek op dat font-display: optional ervoor kan zorgen dat de gebruikte lettertypenset verschilt van de gedeclareerde set. Een voltooide meting kan de terugvaltekst dus correct beschrijven. Document: fonts property.

Bepaal voordat je de wachtcode schrijft welk resultaat je nodig hebt:

  • Meet het huidige weergegeven resultaat: wacht tot de lettertypen gereed zijn en lees de DOM-geometrie uit, waarbij je de terugvalkeuzes van de browser accepteert.
  • Vereis een gedeclareerd en downloadbaar lettertype: laad het overeenkomende lettertype expliciet, handel fouten af en wacht daarna tot de documentlettertypen gereed zijn voordat je meet.
  • Meet veranderende inhoud: herhaal de volgorde na elke relevante wijziging en verwerp resultaten die door een volgende wijziging zijn achterhaald.

Voor het tweede resultaat accepteert document.fonts.load() een CSS-lettertypespecificatie en optionele voorbeeldtekst. Met een synthetische, gedeclareerde lettertypefamilie genaamd SampleFace kan de specificatie 400 18px \"SampleFace\" zijn en de tekst Panel Alpha. De geretourneerde promise wordt afgewezen als een overeenkomend lettertype niet kan worden geladen. Het tekstargument selecteert lettertypen op basis van hun gedeclareerde Unicode-bereiken; het controleert niet of afzonderlijke glyphs aanwezig zijn. FontFaceSet: load() method.

Vervang deze vereiste niet door document.fonts.check(). MDN legt uit dat deze methode true kan retourneren, zelfs als het gevraagde lettertype niet beschikbaar is. Ook bepaalt de W3C dat ready nooit wordt afgewezen; een geslaagde afronding bewijst dus op zichzelf niet dat het laden van lettertypen is gelukt. FontFaceSet: check() method, CSS Font Loading: The ready attribute.

Voer een volledig voorbeeld uit, van stijlblad tot meting

Open op je Mac about:blank in een actuele desktopbrowser, open de ontwikkelaarstools en selecteer Console. Voer het volgende volledige JavaScript-programma één keer uit. Het maakt zelf neutrale voorbeeldtekst en een stijlblad aan, dus je hebt geen server, bibliotheek of lettertypebestand nodig.

Het voorbeeld gebruikt bewust system-ui. Zo wordt de wachtvolgorde gedemonstreerd zonder afhankelijk te zijn van een downloadbaar bestand. Behoud op een pagina die webfonts gebruikt de volgorde en laat de werkelijke lettertypedeclaraties van die pagina de bronnen leveren. Het browservoorbeeld is voor dit manuscript gecontroleerd, maar niet uitgevoerd.

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

De belangrijke volgorde is: het stijlblad is gereed, de lay-out van het doel is ingesteld, de lettertypen zijn gereed en daarna worden de uiteindelijke geometriewaarden uitgelezen. De eerste geometrie-uitlezing stelt de huidige lay-out vast; de waarde wordt bewust weggegooid. De laatste uitlezingen beschrijven het resultaat nadat de lettertypen gereed zijn. De deadline van tien seconden is een voorbeeld van een praktische limiet: bij het verstrijken ervan wordt een niet-voltooide poging gemeld en vindt er geen meting plaats.

Bekijk fontSize en lineHeight samen met de kaders wanneer je het voorbeeld aanpast. getComputedStyle() retourneert de opgeloste eigenschapswaarden nadat actieve stijlbladen zijn toegepast. Dat is handig om een afwijking in een selector of de cascade op te sporen. De waarde fontFamily beschrijft de CSS-lijst met lettertypefamilies; die bewijst niet welk lettertype voor elke glyph is gebruikt. Window: getComputedStyle() method.

Welke DOM-meting geeft antwoord op je vraag?

Gebruik element.getBoundingClientRect() als je de buitenruimte nodig hebt die een kaart of alinea inneemt. De rechthoek omvat padding en randen, en de positie ervan is relatief aan de viewport. In het voorbeeld is de opgegeven border-box-breedte 280px; de beschikbare inhoudsbreedte voor de tekst is 254px, berekend als 280 − 24 − 2. Die berekening beschrijft de opgegeven CSS, niet een waargenomen browserresultaat. Element: getBoundingClientRect() method.

Gebruik een Range over de tekst als je de lay-outfragmenten ervan nodig hebt. Range.getClientRects() retourneert rechthoeken voor het bereik, niet alleen voor het omvattende alinea-element. Bewaar de verzameling: tekst met regelterugloop samenvoegen tot één breedte verliest nuttige informatie over de ingenomen gebieden. Noem de lengte van de array in een algemene implementatie niet lineCount; complexe inline-inhoud kan meerdere rechthoeken opleveren. Range: getClientRects() method.

Laat de oorspronkelijke tekstrun intact tijdens het meten. Een wrapper om elk teken kan opmaakgrenzen introduceren die de vormgeving beïnvloeden. In de W3C-ontwerptekst voor CSS Text staan de voorwaarden waaronder inlinegrenzen de vormgeving behouden of onderbreken. Door het bestaande DOM met een bereik te meten, vermijd je zulke wrappers. CSS Text Module Level 3: Shaping Across Element Boundaries.

Canvas heeft een andere functie. measureText() retourneert canvas-tekstmetingen voor een opgegeven tekenreeks. Voor deze taak — bepalen hoe tekst daadwerkelijk wordt afgebroken in een DOM-kaart — lees je de DOM uit nadat alles gereed is. Behandel een canvasmeting als een afzonderlijke berekening met eigen tekstinstellingen. CanvasRenderingContext2D: measureText() method.

Wat moet je doen als metingen blijven veranderen?

Gebruik deze diagnosevolgorde voordat je nog een vertraging toevoegt:

  1. Controleer of het stijlblad waarop je hebt gewacht het stijlblad is dat het doel opmaakt. Houd bij dynamisch ingevoegde stijlbladen hun laadpromises bij de invoegcode.
  2. Bekijk de opgeloste typografie en breedte van het doel. Een geladen stijlblad kan in de cascade alsnog verliezen.
  3. Controleer of de doeltekst en de bedoelde container aanwezig zijn voordat je wacht tot de lettertypen gereed zijn.
  4. Bepaal of terugval acceptabel is. Als een specifiek gedeclareerd lettertype vereist is, laad het dan expliciet en meld fouten.
  5. Controleer of een andere wijziging van de toepassing de tekst, klassen of container na het wachten heeft veranderd. Start de meting opnieuw voor die nieuwe toestand.

Een framecallback is geen barrière voor het laden van bronnen. requestAnimationFrame() plant een callback in vóór een nieuwe repaint, en browsers pauzeren deze vaak in achtergrondtabbladen. Gebruik de callback wanneer frametiming nuttig is voor je toepassing, maar vervang promises voor stijlbladen of lettertypen niet door één of twee framecallbacks. Window: requestAnimationFrame() method.

Observeer bij een aanpasbare lay-out de relevante container met ResizeObserver en plan nieuwe metingen in wanneer de afmetingen veranderen. De observer meldt wijzigingen in de content-box- of border-box-afmetingen; het is geen universele melding dat tekst is veranderd. Start metingen ook vanuit de toepassingsbewerkingen die tekst vervangen of de typografie wijzigen. ResizeObserver.

Als je Playwright al gebruikt, voer de wachtstap voor browsergereedheid en de geometrieuitlezingen aan de browserzijde uit binnen een asynchrone page.evaluate()-callback. Playwright wacht op de geretourneerde promise en stuurt het resultaat terug naar de aanroepende omgeving. Retourneer gewone numerieke rechthoekvelden, zoals het voorbeeld doet, en geef waarden uit de aanroepende omgeving expliciet door aan de callback. Evaluating JavaScript.

Een bruikbare meting hoort bij een bepaalde tekst, toestand van stijlbladen, uitkomst voor lettertypen en containerafmeting. Stel die invoerwaarden vast, wacht op het relevante laadwerk en meet het intacte DOM. Herhaal dit wanneer die invoerwaarden veranderen, zodat de vastgelegde geometrie antwoord blijft geven op de lay-outvraag die je daadwerkelijk hebt.

Bronnen