Symptom
After replacing render-blocking Google Fonts <link>s with self-hosted @fontsource/source-serif-4 5.3.0 statics (300/400/600/400i), 16px body text in a dark theme looked wider, chunkier-serifed and 'noisy'; lines rewrapped (fewer words per line). Headlines looked slightly heavier.
Cause
The old request was family=Source+Serif+4:ital,opsz,wght@0,8..60,300;.... Google served a variable font with the opsz axis (122 KB latin normal, all weights), and Chrome's default font-optical-sizing: auto set opsz = font-size in CSS px (16 for body, ~34 for a 34px title).
@fontsource/<family> static files are named instances with no fvar. Their STAT table shows the pinned optical size:
from fontTools.ttLib import TTFont # needs brotli for woff2
t = TTFont('node_modules/@fontsource/source-serif-4/files/source-serif-4-latin-300-normal.woff2')
n = t['name']
print([(getattr(v,'Value',None), n.getDebugName(v.ValueNameID)) for v in t['STAT'].table.AxisValueArray.AxisValue])
# [(14.0, '14pt'), (300.0, 'Light'), (0.0, 'Roman')]Newsreader statics report '16pt'. So every size renders at opsz 14 (Source Serif 4) / 16 (Newsreader). A fontTools instance at wght=300, opsz=14 is pixel-identical to the fontsource static in headless Chrome; opsz=16 reproduces Google's line breaks.
Fix
Source the @fontsource-variable/<family> *-latin-opsz-{normal,italic}.woff2 (wght+opsz axes) and instance with fontTools.varLib.instancer.instantiateVariableFont: pin wght and opsz for faces used at one size (Source Serif 4 300 @ opsz 16 = 20.9 KB, same as the static), keep an opsz range only for display weights (600, opsz 16..60 = 46 KB). Commit the outputs plus the script. Shipping the full opsz variable files instead restores parity at every size but costs ~120-130 KB per style.
Verify by screenshot diff against the Google CSS, not by eye: inject Google's css2 under a renamed family and compare line breaks.