How Browsers Measure Text
- Overview
An interactive explorable explanation of browser text measurement — from Canvas measureText() and Intl.Segmenter to greedy line-breaking, variable-width reflow, and shrinkwrap binary search. Build intuition for the prepare/layout two-phase architecture that powers modern text-heavy applications.
- Technologies
- Text LayoutCanvas APIIntl.SegmenterInteractiveBrowser APIsPerformance

The Character
When you call ctx.measureText('A'), what do you actually get back? Most developers only use .width. But the TextMetrics object returns much more: bounding boxes, ascent, descent, and font-level boundaries that tell you exactly where the ink lands.
Notice how width and the actual bounding box rarely agree. A lowercase “g” descends below the baseline; an emoji may extend far beyond the font's reported width. When you need pixel-perfect layout, bounding boxes are what matter.
The Word
The Segmenter
The Line
Now we have segments with measured widths. Line-breaking is just arithmetic: accumulate widths left-to-right until the next segment would overflow, then break. No DOM. No canvas. Just numbers.
0 lines at 320px, 0px tall
Greedy line-breaking: pure arithmetic over pre-measured widths.
function layout(prepared, maxWidth, lineHeight) {
const lines = [];
let cursor = 0;
let y = 0;
while (cursor < prepared.segments.length) {
const line = [];
let lineWidth = 0;
while (cursor < prepared.segments.length) {
const seg = prepared.segments[cursor];
if (line.length > 0 && seg.isWordLike
&& lineWidth + seg.width > maxWidth) {
break; // overflow — start new line
}
line.push(seg);
lineWidth += seg.width;
cursor++;
}
lines.push({ segments: line, width: lineWidth, y });
y += lineHeight;
}
return { lines, lineCount: lines.length, height: y };
}The Resize
The prepare step is expensive — segmenting and measuring text takes real time. But it only runs once. The layout step is pure arithmetic over cached widths. How fast is “pure arithmetic”?
Predicted 0px, actual 0px
At scale 1, both approaches are near-instantaneous. Drag the scale to 1000 and the gap becomes dramatic: arithmetic layout stays flat while DOM measurement climbs. The prepare-once, layout-many architecture makes resize essentially free.
The Obstacle
CSS shape-outside lets text flow around static shapes. But what if the shape moves? Each overlapping line queries the shape’s actual edges at that y-position — not a bounding box. Text fills both sides of the shape at 60fps.
Drag the obstacle, or focus it and use the arrow keys to move it. Hold Shift to move further. The text reflows around it.
0 lines, 0 split around the obstacle
Each line queries the hexagon's actual edges at that y-position. Near the tips, more text fits. In the middle, less.
for each line at y:
// get the shape's actual edges at this y
const shape = hexBoundsAtY(y - obstacle.top);
if (shape) {
leftWidth = obstacle.x + shape.left - gap;
rightWidth = container - obstacle.x - shape.right - gap;
// fill both sides
} else {
// full-width line
}The Shrinkwrap
CSS fit-content sizes a container to its longest line — but that's rarely the tightest possible fit. What's the narrowest width that keeps text in exactly N lines? Because our layout function is pure arithmetic, we can binary search for the answer.
Because layout is pure arithmetic, we can binary search over container widths in microseconds.
function findOptimalWidth(prepared, targetLines, lineHeight) {
let lo = 0;
let hi = measureWidth(fullText, font);
while (hi - lo > 1) {
const mid = Math.floor((lo + hi) / 2);
const result = layout(prepared, mid, lineHeight);
if (result.lineCount <= targetLines) {
hi = mid; // fits — try narrower
} else {
lo = mid; // too many lines — go wider
}
}
return hi;
}Putting It All Together
Where This Matters
Virtualized Chat
Slack, Discord, Teams — thousands of messages that need height prediction for smooth scrolling without rendering every message to the DOM.
Document Editors
Notion, Google Docs — paginate and reflow on resize without triggering layout thrashing. Pagination requires knowing where page breaks fall before rendering.
Masonry Layouts
Pinterest, image galleries — predict card heights for positioning without rendering offscreen. Eliminates the layout shift when scrolling fast.
Collaborative Tools
Real-time reflow as collaborators edit. Text measurement that runs in a Web Worker via OffscreenCanvas — no main thread blocking.
This explorable was inspired by Cheng Lou’s pretext library, which uses the same core ideas — Intl.Segmenter, Canvas measureText, and greedy line-breaking — to do text layout without touching the DOM. It also handles edge cases we glossed over: Japanese kinsoku rules, emoji width correction, RTL bidirectional text, and sub-pixel rounding differences across browsers.
Related notes

CarTalk: From 8-Second Pages to 300ms
CarTalk brought me in to pay down years of technical debt on its site. Moving to Next.js and TypeScript, static generation and deduplicated API calls cut page loads from 8 seconds to 300ms and builds from 10 minutes to under 2, and left the team with a roadmap it could keep.

Agent Harness 101
Every coding agent is a short loop: ask the llm, run the tools it asks for, and stop when it asks for nothing. The harness is everything around that loop that keeps it safe and durable, and each of its primitives (session, thread, turn, tool, permission and agent) is written here as a real schema. Ends with a scripted demo that follows one message from the moment you press enter to the final answer.

Take the Keys Away From Your Agent
A process that runs model-authored code should hold no credential worth stealing and have no route to anywhere it could send one. Builds that from the bottom up: what TCP, HTTP and TLS actually are, how certificate authorities decide who you believe, and what a proxy can and cannot see. Ends with an interactive walkthrough of one request through every process that touches it, which you can rerun on a flat network to watch the whole thing become irrelevant.
Get in touch
Have a question about something I wrote, or want to talk? Send me a message.