Check B5 divides the text on a page by the markup it took to deliver it. At or above 0.10 the page earns all three points; from 0.05 up to 0.10 it earns two; below 0.05 it earns one. The number looks like a page-weight metric and is not one. Since rubric v1.0.1 the contents of script and style elements are removed before anything is counted, so what remains is a measure of how much markup you spend per character of content. This post is the engineering of that measurement: what is in the numerator, what is in the denominator, what pushes the ratio down, and a way to compute it from a terminal that tracks the scanner closely enough to act on.
What the scanner counts
B5 runs on the raw HTML of every crawled page that returned a 200 with a non-empty body. Raw means the response as fetched, before any JavaScript runs, which is the same document the two fetches describes an agent receiving. Each page gets its own ratio, and the score is taken from the mean of those ratios rather than from pooled totals.
The numerator is the text of the page. The scanner parses the document, removes every script, style, noscript, template and svg element, takes the remaining text content, collapses runs of whitespace to a single space, trims, and counts characters.
The denominator is the markup. The scanner takes the raw HTML string, deletes every <script>…</script> and <style>…</style> element (tags included), and takes the length of what is left. For ASCII markup, which is nearly all of it, that length is the byte count. Everything else stays: tags, attributes, comments, inline SVG, noscript fallbacks, template elements.
Two asymmetries follow from that. First, svg, noscript, template and HTML comments are removed from the numerator and left in the denominator. They are pure cost: an inline icon contributes nothing to the text and every one of its bytes to the markup. Second, JSON-LD lives in a script element, so it is removed from both sides. Adding structured data for B1 and B2 costs nothing on B5.
The report records the mean as averageRatio and each page's value as perPage, both rounded to three decimal places. Note the floor: B5 cannot score zero. The worst outcome is one point of three.
| Average ratio | Points |
|---|---|
| 0.10 or above | 3 |
| 0.05 up to 0.10 | 2 |
| Below 0.05 | 1 |
For orientation, 0.10 means ten bytes of markup for every character of text. A plain article page with sensible templates sits comfortably above that. A page at 0.03 is delivering thirty bytes of scaffolding per character, and somewhere in it there is a specific cause.
Why script and style are excluded
Under v1.0.0 the denominator was the whole document. When we scored our own site we found that the serialised payload our server-rendering framework inlines, the one that duplicates the page text so the browser can hydrate it, was 60% of our homepage. Measured against total bytes, B5 was penalising exactly the server-rendered architecture that A7 rewards. It was measuring framework choice rather than markup quality.
Version 1.0.1, dated 2026-08-07, removes script and style contents from the denominator. The entry on the changelog records the change and the reason, and scores issued under v1.0.0 are not restated. Why we publish the rubric makes the case for handling changes that way; this post is about what the change did to the measurement.
What it did is narrow the check to a single question. A page with 400 KB of scripts and a ratio of 0.2 passes B5. Those scripts may still cost the site elsewhere, and A7 still asks whether the text is present before they run, but B5 no longer has an opinion about them. Equally, a page with no scripts at all can fail it, if the markup around the text is heavy enough. Server-rendering frameworks, Next.js among them, inline their payloads in script elements, so the exclusion catches the common case. If a framework serialises state into attributes or hidden elements instead, that still counts, because it is markup.
What inflates the denominator
The scanner's recommendation text says it plainly: this is markup weight, not framework overhead. In practice the weight comes from a short list of sources.
Inline SVG icons. A single icon path runs to a kilobyte or more; a logo, several. A navigation bar and footer with forty inline icons can carry more bytes than the page's entire text, and every one of those bytes lands on the wrong side of the division. The fix is a sprite referenced with <use>, an img pointing at a file, or a CSS mask. None of these changes what a visitor sees.
Utility-class attributes and wrapper depth. Attributes are markup. A class attribute listing a dozen utility names on every element, multiplied by every element, is the most common reason a clean-looking site scores two rather than three. Wrapper nesting compounds it, because each wrapper is an element with attributes and no text.
<div class="relative flex min-h-0 w-full flex-col items-stretch">
<div class="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between gap-x-6 py-4">
<div class="flex shrink-0 items-center gap-x-2">
<span class="text-sm font-medium text-gray-700">Pricing</span>
</div>
</div>
</div>
</div>
That is seven characters of text and around 360 of markup, a ratio of 0.02 for the block. A page built entirely from blocks like it will not reach 0.05.
Inline style attributes. The style element is excluded; the style attribute is not. Visual page builders tend to emit a style attribute per element, and a page exported from one can carry more bytes of style="…" than of prose.
Hydration attributes, templates and noscript fallbacks. Frameworks that mark islands with data-* attributes, or ship hydration HTML inside template elements, count in full. So does the <noscript><iframe …> fallback a tag manager asks you to paste. These are small on their own and matter when a page is already near a threshold.
Data URIs. An image inlined as src="data:image/png;base64,…" is markup by this measure. A hero background or a blurred placeholder inlined that way can outweigh everything else on the page. Serve it as a file.
Comments. Build tools and CMS plugins leave HTML comments behind. The parser drops them from the text; the denominator keeps them.
Measure it yourself
Fetch the raw HTML and apply the same two rules. This uses regular expressions where the scanner uses a parser, so treat a difference in the third decimal place as noise. It will not disagree with the scanner about which band you are in.
curl -sL https://example.com/ -o page.html
python3 - <<'PY'
import re
html = open("page.html", encoding="utf-8", errors="replace").read()
markup = re.sub(r"<script\b[^>]*>[\s\S]*?</script>", "", html, flags=re.I)
markup = re.sub(r"<style\b[^>]*>[\s\S]*?</style>", "", markup, flags=re.I)
text = re.sub(r"<(svg|noscript|template)\b[^>]*>[\s\S]*?</\1>", "", markup, flags=re.I)
text = re.sub(r"<!--[\s\S]*?-->", "", text)
text = re.sub(r"<[^>]+>", " ", text)
text = re.sub(r"\s+", " ", text).strip()
print(f"text {len(text)} markup {len(markup)} ratio {len(text)/len(markup):.3f}")
PY
If the ratio is low, the next question is where the bytes are, and the same file answers it.
python3 - <<'PY'
import re
html = open("page.html", encoding="utf-8", errors="replace").read()
html = re.sub(r"<script\b[^>]*>[\s\S]*?</script>|<style\b[^>]*>[\s\S]*?</style>", "", html, flags=re.I)
for label, pattern in [
("inline svg", r"<svg\b[\s\S]*?</svg>"),
("class attributes", r'\sclass="[^"]*"'),
("style attributes", r'\sstyle="[^"]*"'),
("data URIs", r'src="data:[^"]*"'),
("comments", r"<!--[\s\S]*?-->"),
]:
print(f"{label:18s} {sum(len(m) for m in re.findall(pattern, html, flags=re.I)):8d}")
PY
Run both against one page per template rather than the homepage alone. The scanner averages across the crawl, and the page that drags the average is often a listing or a product page rather than the one you look at most.
What to do at each band
Below 0.05, there is almost always one dominant cause, and the second script names it: a data URI, a page builder's style attributes, or a wall of inline SVG. Remove that and the page usually crosses 0.05 in a single change.
Between 0.05 and 0.10, the cause is diffuse: wrapper depth and class attributes, spread across every component. That is why the rubric rates B5 as Medium effort, weeks rather than hours in the terms the three tiers of fixes uses. The work is flattening the component tree and moving repeated utility stacks into a stylesheet, where they are excluded from the count.
Two things not to do. Do not chase the ratio by cutting content; the check rewards text, and a page that says more scores higher. And do not move text into a script to be injected later, which improves B5 at the cost of A7 and of the agent ever seeing the text. Plain paragraphs in plain elements are the cheapest fix there is, and they are also what an agent came for.
Check your own site
Run the free scan and read averageRatio and perPage under B5, then point the scripts above at the lowest page. The full definition of the check, and the v1.0.1 entry that changed it, are on the methodology page.