Printing and PDFs
A printed page is a different medium, not a screenshot of a screen. The Utilities module
carries a print block that switches the palette to black on white, drops navigation,
footers, sidebars, tab controls and copy buttons, opens every tab panel and accordion so
nothing hides behind a control nobody can click, and prints the address after each external
link — read more is useless once the page is paper.
It also sets page-break rules, because a heading stranded at the foot of a page and a
table split across two are what make a printed reference unusable: headings avoid a break
after, tables and cards avoid a break inside, and thead repeats on every page.
| Class | On screen | On paper |
|---|---|---|
.pf-no-print | Visible | Hidden |
.pf-print-only | Hidden | Visible |
Use the second for a note or a short URL that belongs on paper and nowhere else. Print any page of this site to see the rest of it.
Font loading and layout shift
The gap between a fallback face rendering and the web font arriving is where cumulative layout shift comes from: Arial is shorter and wider than Inter, so every line box changes height and every heading rewraps at the moment of the swap.
The Fonts module declares three metric-matched fallback faces — Arial and Courier New with the real fonts' ascent, descent and average width forced onto them — and each stack names its fallback before the generic families. The browser lays the page out at the right line height and the right width immediately, then the real font lands into a box that already fits.
| Face | Units/em | Ascent | Descent | Average width |
|---|---|---|---|---|
| Inter | 2048 | 96.9% | 24.1% | 610/1000 |
| Bricolage Grotesque 700 | 1000 | 93.0% | 27.0% | 539/1000 |
| IBM Plex Mono | 1000 | 102.5% | 27.5% | 600/1000 |
| Arial (the fallback) | 2048 | 90.5% | 21.2% | 500/1000 |
Every figure is read out of the shipped woff2 files with fontTools
rather than estimated. size-adjust is the ratio of the two average widths.
What a page actually costs
Pick the kind of page you are building. The figures are measured at build time, not estimated — per-file minified size, the gzipped total, and how many requests it takes.
Sign-in or error screen · Core + Content
Two requests. No navigation, no footer, no utilities — the sign-in example loads exactly this.
Marketing page · Core + Headers + Content + Footers
Four requests, and the whole visual language of a landing page. Fonts add 152 KB once, cached for a year.
Documentation page · the four above plus Space
Space is the largest module and the cheapest gzipped — its strings repeat, which is exactly what deflate is good at.
Application screen · Core, Headers, Content, Forms, Components
Five requests. Add the optional script at 3 KB gzipped if you want sortable tables and a real tablist.
All eleven modules · the bundle
One request, and smaller gzipped than eight separate modules — which is why the build ships the bundle past that threshold.
Bars are proportional width utilities, not inline styles. Figures come from the build, so they cannot drift from the files.
What it costs
| Module | Minified | Gzipped | Load it when |
|---|---|---|---|
| Core | 14.3 KB | 4.1 KB | Always |
| Headers | 6.7 KB | 1.8 KB | The page has a nav or a hero |
| Content | 14.7 KB | 3.3 KB | Almost always |
| Components | 16.9 KB | 3.5 KB | App UI: tabs, menus, drawers, toasts |
| Space | 19.8 KB | 2.8 KB | You want spacing utilities |
| Grid | 12.6 KB | 1.6 KB | Proportions change per breakpoint |
| Utilities | 8.9 KB | 2.3 KB | Optional helpers |
| Forms | 5.4 KB | 1.5 KB | Beyond the four basic controls |
| Footers | 3.5 KB | 1.0 KB | The page has a footer |
| Fonts | 2.6 KB | 0.4 KB | You want the shipped typefaces |
| polyfish.js | 9.6 KB | 3.6 KB | Optional, never required |
Everything concatenated is 102.3 KB minified and 17.9 KB gzipped. The gap between those two numbers is the point: a utility surface is highly repetitive text and compresses accordingly, which is why Space is the largest file minified and one of the smallest over the wire.
What to load
Four real page types, with what they actually need.
Marketing page
Core, Headers, Content, Footers, Space, Utilities.
67.1 KB min · 15.2 KB gzip
Documentation
Core, Headers, Content, Space, Utilities. No footer module.
63.6 KB min · 14.2 KB gzip
Application shell
Core, Headers, Content, Forms, Components, Space, Utilities.
86.4 KB min · 19.2 KB gzip
Sign-in screen
Core and Content. Nothing else — no nav, no footer, no grid.
28.2 KB min · 7.3 KB gzip
One request or several?
Under HTTP/2 the per-request cost is small, so five module files and one bundle are close to equivalent on first load. Separate files win on repeat visits — change Content and the visitor re-downloads 3.3 KB rather than 17.7 KB, because the other files keep their year-long immutable cache. Use the bundle while prototyping, split when you ship.
Fonts are the real budget
The three self-hosted families total 166.6 KB, which is more than every stylesheet combined. That is normal and it is where the attention belongs.
- Two faces are preloaded, not seven: the body regular and the display bold, the only ones needed above the fold.
crossoriginis mandatory on a font preload — without it the file downloads twice. font-display: swapmeans text paints immediately in the fallback and repaints when the face arrives. Never invisible, never blocking.- Latin subsets only. Add a subset by dropping the file in
/fontsand copying a block; the framework never asks for a weight that is not declared. - Skip the module entirely and the system stacks take over with no layout change. That is 166.6 KB and a whole class of loading behaviour gone.
Layout shift
Cumulative layout shift is mostly caused by content arriving late into space that was never reserved. Four places the framework reserves it for you.
| Cause | What holds the space |
|---|---|
| Images | .pf-tile and .pf-ratio use aspect-ratio, so the box exists before the file arrives. |
| Embeds | .pf-ratio--16x9 and friends hold an iframe's box from first paint. |
| Async content | .pf-skeleton at the shape of what is coming, marked aria-busy. |
| Fonts | swap still reflows when the face lands. The fallback stacks are chosen for similar metrics to keep that shift small. |
Serving it
The published server configuration does four things that matter more than any byte count.
- Immutable caching on CSS, JS, fonts and themes:
max-age=31536000, immutable. A repeat visit fetches nothing. Version the filename when the content changes. must-revalidateon HTML, so a page is never stale while its assets are cached forever.- gzip on, with
text/htmlleft out ofgzip_types— nginx compresses HTML unconditionally and listing it logs a duplicate-MIME warning on every start. Brotli lines are included, commented, for when the module is available. - CORS on fonts and the script, because both are fetched in CORS mode. Without it the font preload is discarded and the subresource-integrity check on the script cannot run.
The number that is always zero
No page on this site makes a request to another origin. No font CDN, no analytics, no tag manager. That is a privacy decision first and a performance one second, but it also removes every DNS lookup and TLS handshake that is not to your own server.
Next: the optional script.