Skip to content
PolyFish CSS

Performance

Every number here is measured from the shipped files rather than estimated, and regenerated whenever they change. Sort the table by the gzipped column — that is the one that arrives over the wire.

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.

ClassOn screenOn paper
.pf-no-printVisibleHidden
.pf-print-onlyHiddenVisible

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.

FaceUnits/emAscentDescentAverage width
Inter204896.9%24.1%610/1000
Bricolage Grotesque 700100093.0%27.0%539/1000
IBM Plex Mono1000102.5%27.5%600/1000
Arial (the fallback)204890.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.

page type

Marketing page · Core + Headers + Content + Footers

Core14 KB
Headers7 KB
Content15 KB
Footers4 KB
over the wire10.9 KB gzip

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

four modules40 KB
Space20 KB
over the wire12.3 KB gzip

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

Core14 KB
Headers7 KB
Content15 KB
Forms5 KB
Components17 KB
over the wire13.9 KB gzip

Five requests. Add the optional script at 3 KB gzipped if you want sortable tables and a real tablist.

All eleven modules · the bundle

PolyFishCSS.min108 KB
over the wire19.3 KB gzip

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

ModuleMinifiedGzippedLoad it when
Core14.3 KB4.1 KBAlways
Headers6.7 KB1.8 KBThe page has a nav or a hero
Content14.7 KB3.3 KBAlmost always
Components16.9 KB3.5 KBApp UI: tabs, menus, drawers, toasts
Space19.8 KB2.8 KBYou want spacing utilities
Grid12.6 KB1.6 KBProportions change per breakpoint
Utilities8.9 KB2.3 KBOptional helpers
Forms5.4 KB1.5 KBBeyond the four basic controls
Footers3.5 KB1.0 KBThe page has a footer
Fonts2.6 KB0.4 KBYou want the shipped typefaces
polyfish.js9.6 KB3.6 KBOptional, 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. crossorigin is mandatory on a font preload — without it the file downloads twice.
  • font-display: swap means 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 /fonts and 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.

CauseWhat 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.
Fontsswap 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-revalidate on HTML, so a page is never stale while its assets are cached forever.
  • gzip on, with text/html left out of gzip_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.

Other things you can poke

Every documentation page on this site has something interactive in it: swap page regions, retint a live page, change the type scale, see what each module unlocks, assemble your link block, frame a real page at 320px, weigh a page type, build an attribute list, turn the invisible guards on, flip a component through its states and filter the changelog by tag. All of it is radio inputs, checkboxes and :has() — no script runs any of it.