<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahmed Nader</title>
    <description>The latest articles on DEV Community by Ahmed Nader (@ahmed_nader).</description>
    <link>https://dev.to/ahmed_nader</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4154666%2Fd13ea43f-d1af-4ff4-989b-c90217e96e2c.png</url>
      <title>DEV Community: Ahmed Nader</title>
      <link>https://dev.to/ahmed_nader</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed_nader"/>
    <language>en</language>
    <item>
      <title>Google Fonts Hands You a 1.5 MB Font. Your Page Needs 26 KB of It</title>
      <dc:creator>Ahmed Nader</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:45:38 +0000</pubDate>
      <link>https://dev.to/ahmed_nader/google-fonts-hands-you-a-15-mb-font-your-page-needs-26-kb-of-it-3fph</link>
      <guid>https://dev.to/ahmed_nader/google-fonts-hands-you-a-15-mb-font-your-page-needs-26-kb-of-it-3fph</guid>
      <description>&lt;p&gt;Go to Google Fonts, pick a family, Download it, and drop the file in your project. For Noto Sans Display a fine specimen of mundane webfont — that file is &lt;strong&gt;1,540,036 bytes&lt;/strong&gt;. One and a half million bytes of outlines, for a landing page that renders about ninety distinct characters.&lt;/p&gt;

&lt;p&gt;As usual, the advice is "use font-display and move along" But file size is not vibes, it's arithmetic, so I did the arithmetic: opened the font file, counted what's in it, whittled it down, and stress-tested the outcome. These are the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The census
&lt;/h2&gt;

&lt;p&gt;The file you downloaded is the &lt;em&gt;variable&lt;/em&gt; font: one file carrying every weight from Thin to Black plus a width axis — that's why it's 1.5 MB instead of the 350 KB a single static weight costs. Inside, both versions tell the same story:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Variable download&lt;/th&gt;
&lt;th&gt;Static Regular&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Glyphs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,316&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3,316&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple outlines (drawn directly)&lt;/td&gt;
&lt;td&gt;1,796&lt;/td&gt;
&lt;td&gt;1,796&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composites (references to other glyphs)&lt;/td&gt;
&lt;td&gt;1,487&lt;/td&gt;
&lt;td&gt;1,487&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty slots (structural, no ink)&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bytes&lt;/td&gt;
&lt;td&gt;1,540,036&lt;/td&gt;
&lt;td&gt;357,864&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character map&lt;/td&gt;
&lt;td&gt;5,680 codepoints&lt;/td&gt;
&lt;td&gt;5,680&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the 1,487. Nearly half the glyphs in this font are composites, a concept the "bag of shapes" mental model can't express and will find out in a minute.&lt;/p&gt;

&lt;p&gt;Meanwhile, your landing page renders roughly ninety distinct characters.&lt;br&gt;
English copy, some punctuation, digits. The rest is ink for Cyrillic, Greek, five styles of typographic quotation mark, and mathematical operators your marketing copy has never met.&lt;/p&gt;
&lt;h2&gt;
  
  
  The subset, measured
&lt;/h2&gt;

&lt;p&gt;The keep-list for a typical English site: Basic Latin (U+0020–007E), Latin-1 Supplement (U+00A0–00FF, so accented characters in borrowed words don't boxout), the General Punctuation block for real quotes and dashes, plus € and the true minus sign. Now the interesting fork in the road:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subset the variable file, keep it variable.&lt;/strong&gt; The subsetter carves the &lt;em&gt;variation data&lt;/em&gt; down to the surviving glyphs along with the outlines. What comes back is still a genuine variable font — both axes, every weight from 100 to 900, every width — for 349 glyphs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Glyphs&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;th&gt;% of download&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The download, as-is&lt;/td&gt;
&lt;td&gt;3,316&lt;/td&gt;
&lt;td&gt;1,540,036&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Landing-page subset, &lt;strong&gt;still variable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;349&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;135,048&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instanced to Regular, then subset&lt;/td&gt;
&lt;td&gt;349&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26,148&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That second row is a legitimately great option nobody talks about: &lt;strong&gt;all nine weights of exactly the characters your page uses, in 135 KB.&lt;/strong&gt; Bold headlines, light captions, one file, full design freedom.&lt;/p&gt;

&lt;p&gt;If you ship one weight — say, Regular and let the browser synthesize nothing&lt;br&gt;
— pin the axes first (instancing, 1.5 MB → 356 KB, which is the static font regrown from the VF) and &lt;em&gt;then&lt;/em&gt; subset: &lt;strong&gt;26,148 bytes&lt;/strong&gt;, byte-for-byte the same result as subsetting the static file. That's a 98.3% cut of the file you actually downloaded. One time, zero runtime cost.&lt;/p&gt;

&lt;p&gt;Both outputs are complete, valid fonts. Not amputated ones — and the reason 349 glyphs come back when you asked for ~90 characters is the whole trap below.&lt;/p&gt;
&lt;h2&gt;
  
  
  The é test
&lt;/h2&gt;

&lt;p&gt;Here's the mistake that gives subsetting its bad reputation.&lt;/p&gt;

&lt;p&gt;In this font, é is not drawn. It's a &lt;em&gt;composite&lt;/em&gt; — a tiny record saying "take &lt;strong&gt;e&lt;/strong&gt;, take the combining acute accent, stack them here." Remember: 1,487 of our 3,316 glyphs are these references.&lt;/p&gt;

&lt;p&gt;Now suppose your keep-list contains é but not e — say you're shipping a French page and decide a bare "e" isn't on it. A naive subsetter deletes the e glyph obediently. What happens to é? It's still in the font, still pointing at a glyph that no longer exists. Your page renders a box where é should be.&lt;br&gt;
&lt;strong&gt;The character you kept breaks because of the character you removed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A correct subsetter walks the reference chain before deleting anything. I tested this on the same font: requested exactly one character, U+00E9, nothing else. What came back from the static file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 glyphs, 1,456 bytes&lt;/li&gt;
&lt;li&gt;the é composite itself&lt;/li&gt;
&lt;li&gt;the outline of &lt;strong&gt;e&lt;/strong&gt; — the same 2 contours, 32 points I counted in the original&lt;/li&gt;
&lt;li&gt;the acute accent it references&lt;/li&gt;
&lt;li&gt;two bookkeeping glyphs (.notdef and friends)&lt;/li&gt;
&lt;li&gt;and a character map that maps &lt;em&gt;only&lt;/em&gt; U+00E9&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It passes on the variable font too: 5 glyphs, 5,100 bytes, still variable on both axes, é still a live composite over its surviving components.&lt;/p&gt;

&lt;p&gt;That last line is the subtle part, and it's correct behavior: e's &lt;em&gt;ink&lt;/em&gt; lives in the file (é needs it to render), but e is no longer typeable — it's not in the character map, so no keyboard can reach it. The subsetter kept the drawing and dropped the identity. If your tool instead hands you a font where é renders as a box, it doesn't walk composites, and no amount of "but I kept é!" will save the file.&lt;/p&gt;

&lt;p&gt;This test is one line to run and it's the single best quality check for any subsetting tool. If é survives the deletion of e, the tool understands fonts.&lt;br&gt;
If it doesn't, don't ship that output.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I got wrong on the way
&lt;/h2&gt;

&lt;p&gt;My first verification of that é subset was broken, and it's worth admitting how. I checked whether the output still contained a glyph &lt;em&gt;named&lt;/em&gt; &lt;code&gt;e&lt;/code&gt;. It didn't — names get renumbered or dropped during subsetting — so for a moment I "proved" that the closure had failed and e was gone. The e outline was sitting right there as &lt;code&gt;glyph00001&lt;/code&gt;, identical to the original; I was checking identity by name in a file format that doesn't promise names. The honest test compares outlines and character maps, not labels. Font tooling will happily rename everything under you; test what a glyph &lt;em&gt;is&lt;/em&gt;, not what it's &lt;em&gt;called&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The counterfindings
&lt;/h2&gt;

&lt;p&gt;Three things the headline numbers don't tell you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hinting wasn't part of the win.&lt;/strong&gt; Older fonts carry hinting — pixel-grid instructions that can occupy a real slice of the file. Noto Sans Display ships with none at all (0 bytes of hinting in both the static and the variable builds; modern Google Fonts have dropped it), so there was nothing to strip. On an older hinted font it can matter; on modern ones there's nothing there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subsetting keeps what you keep, including variability — and that cuts both ways.&lt;/strong&gt; The 135 KB variable subset preserves every weight and width &lt;em&gt;of the surviving glyphs&lt;/em&gt;, which is a feature. But if your tool collapses variation tables without telling you — and many GUI tools do — you silently shipped one frozen weight while believing you shipped a VF. Run the subset and check for an &lt;code&gt;fvar&lt;/code&gt; table before you celebrate the file size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't subset your way out of emoji.&lt;/strong&gt; Color emoji fonts embed bitmaps per glyph and are measured in megabytes; the pictures are the point. The 1.7% figure is for outline fonts doing normal text work.&lt;/p&gt;

&lt;p&gt;And one thing that isn't about file size at all: subsetting creates a &lt;em&gt;modified&lt;/em&gt; font, and licenses have opinions. The SIL Open Font License permits modification, but many OFL fonts declare &lt;strong&gt;Reserved Font Names&lt;/strong&gt; &lt;br&gt;
your subset must not ship under the original's name. "I only removed characters" is not a defense any EULA has accepted. Thirty seconds with the license file beats the email from the foundry.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;All the numbers above are one command each, using fontTools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fonttools

&lt;span class="c"&gt;# the census&lt;/span&gt;
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from fontTools.ttLib import TTFont; print(len(TTFont('Font.ttf').getGlyphOrder()))"&lt;/span&gt;

&lt;span class="c"&gt;# the landing-page subset of the VARIABLE font: 1,540,036 -&amp;gt; 135,048 bytes, still variable&lt;/span&gt;
pyftsubset &lt;span class="s2"&gt;"NotoSansDisplay[wdth,wght].ttf"&lt;/span&gt; &lt;span class="nt"&gt;--output-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;subset_vf.ttf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unicodes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"U+0020-007E,U+00A0-00FF,U+2000-206F,U+20AC,U+2212"&lt;/span&gt;

&lt;span class="c"&gt;# or pin the axes first, then subset: -&amp;gt; 26,148 bytes&lt;/span&gt;
fonttools varLib.instancer &lt;span class="s2"&gt;"NotoSansDisplay[wdth,wght].ttf"&lt;/span&gt; &lt;span class="nv"&gt;wght&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;400 &lt;span class="nv"&gt;wdth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 &lt;span class="nt"&gt;-o&lt;/span&gt; regular.ttf
pyftsubset regular.ttf &lt;span class="nt"&gt;--output-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;subset_regular.ttf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unicodes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"U+0020-007E,U+00A0-00FF,U+2000-206F,U+20AC,U+2212"&lt;/span&gt;

&lt;span class="c"&gt;# the é test: request ONE character, check what survives&lt;/span&gt;
pyftsubset regular.ttf &lt;span class="nt"&gt;--output-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eonly.ttf &lt;span class="nt"&gt;--unicodes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"U+00E9"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;eonly.ttf&lt;/code&gt; and render é. If it draws, the tool walks composites.&lt;br&gt;
If it's a box, delete the tool from your pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules worth keeping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Subset the &lt;em&gt;deliverable&lt;/em&gt; — the web embed, the app bundle, the client handoff. Your working library stays full-coverage, or you'll discover mid-project that you need Ł in a headline.&lt;/li&gt;
&lt;li&gt;Decide which deal you want: &lt;strong&gt;135 KB&lt;/strong&gt; for every weight of your characters (still variable), or &lt;strong&gt;26 KB&lt;/strong&gt; for one pinned weight. Both beat 1.5 MB.&lt;/li&gt;
&lt;li&gt;Run the é test on any tool before you trust it — and check whether your VF came out the other side still variable.&lt;/li&gt;
&lt;li&gt;Check the license for reserved font names; rename your subset if needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full disclosure: I build a mobile font engineering tool —&lt;br&gt;
&lt;a href="https://fontstudio.podzi.app/font-studio/font-subsetter/" rel="noopener noreferrer"&gt;Font Studio's subsetter&lt;/a&gt; runs this same closure-preserving subset on-device, which is how I ended up counting glyphs on a phone at midnight in the first place. Whatever tool you use, the numbers above are the bar: &lt;strong&gt;a 1.5 MB download, a 26 KB job.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your users won't notice. Your page weight will.&lt;/p&gt;




&lt;p&gt;Ever shipped a subset that rendered boxes where é should be? Or found a font&lt;br&gt;
in production carrying scripts your app will never render? I'd love to hear the story in the comments.&lt;/p&gt;




</description>
      <category>css</category>
      <category>frontend</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
