← Back to Blog

Self-host the CDN-hosted fonts and scripts. The DNS lookup costs more than the asset.

Self-host the CDN-hosted fonts and scripts. The DNS lookup costs more than the asset.

Most sites still load Google Fonts from fonts.googleapis.com, jQuery from code.jquery.com, Tailwind from cdn.tailwindcss.com, and Bootstrap from cdnjs. The argument used to be that everyone shared the same cached copy across the web, so a returning visitor would skip the download. Browsers killed that optimization in 2018 when they partitioned the HTTP cache per-site for tracking-prevention reasons. Today, every CDN-hosted asset is downloaded fresh per visitor per origin, with all of the cost and none of the benefit.

A new check in the Mega Analyzer flags the public-CDN assets that are good self-host candidates and tells you what to download where. This post explains why each kind of self-host matters and gives the migration recipe for the four most common cases.

The four costs that don't appear on a Lighthouse score

DNS + TLS handshake. Every additional origin on your page adds one DNS lookup, one TCP handshake, one TLS handshake, and one HTTP/2 connection setup before the browser can request the actual file. On a warm connection that is 50-100ms. On a cold mobile connection it is 200-400ms. Loading Google Fonts costs you two of these because the CSS comes from fonts.googleapis.com and the WOFF2 files come from fonts.gstatic.com. That is the full cost on the first load.

Cache fragmentation. Browsers partition the HTTP cache per-origin since 2018 to prevent tracking. So a visitor who downloaded jQuery 3.7.1 from cdn.jsdelivr.net on yourcompetitor.com yesterday will re-download it from your site today. The "shared cache" benefit that justified using public CDNs in 2014 has been dead for seven years. Every CDN asset on your page is downloaded from scratch by every visitor.

Privacy and legal. Public CDN requests leak the visitor's IP, user agent, and referrer to a third party. German and Austrian courts have ruled Google Fonts requests violate GDPR without explicit consent (Munich district court 2022, Austrian DSB 2023). The Schrems II ruling treats every transfer of EU user data to US servers as a separate compliance risk. Self-hosting collapses all of these into a same-origin request, which is what your privacy policy already covers.

Outage exposure. jsdelivr.net had three multi-hour outages in 2024-2025. unpkg.com has had several. cdnjs.cloudflare.com has been more reliable but is not immune. Sites that self-hosted the asset kept rendering. Sites that linked to the public CDN broke. The asset is a build artifact you control; pinning it to a public CDN is taking a dependency on someone else's uptime for no benefit.

What the analyzer flags vs. what it skips

The check flags hosts that are known-stable and known-self-hostable: Google Fonts, jsdelivr, cdnjs, unpkg, code.jquery.com, Bootstrap CDN, fonts.bunny.net, FontAwesome's free CDN, the Tailwind play CDN. Each match comes with a one-line "why this matters" tag.

The check explicitly skips hosts that rotate their bundles by design and cannot be self-hosted without breaking. Those are: Google Tag Manager, Google Analytics, Google Ads, Facebook Pixel, Stripe.js, GoCardless, Intercom, Drift, Tidio, HotJar, Heap, Mixpanel, Segment, Plausible, Microsoft Clarity, LinkedIn pixels, TikTok pixels, X (Twitter) pixels, Snapchat pixels. These are the right kind of CDN dependency. They handle the rotation themselves.

So a Mega Analyzer report that says "5 third-party CDN asset(s) could be self-hosted" never includes your analytics or your payment SDK. It includes the things you actually have control over.

The five-step migration recipe

The recipe is the same shape regardless of asset type. The only thing that changes is what you download.

  1. Download the file once. Use curl or the browser to fetch the asset at the exact version pinned in your HTML. Save it to /vendor/ or /fonts/ in your project. Do not rename. Do not modify.
  2. Hash-pin the file. Generate a SHA384 integrity hash for the version you committed: cat vendor/jquery-3.7.1.min.js | openssl dgst -sha384 -binary | openssl base64 -A. Add it to the script tag's integrity="sha384-..." attribute. The hash never changes because the file is now versioned-and-frozen on your origin.
  3. Long-cache the file. Set Cache-Control: public, max-age=31536000, immutable on /vendor/ and /fonts/. The version is in the URL; the file content is byte-frozen; cache for a year. On Netlify add [[headers]] blocks in netlify.toml. On Cloudflare Pages add the rule to _headers. On any origin server set the Cache-Control on the file directly.
  4. Brotli + HTTP/3. Make sure your origin negotiates brotli compression and HTTP/3 — both add 5-15% throughput improvements over the public CDN's defaults. Most modern hosts (Netlify, Cloudflare Pages, Vercel) ship both by default. If you are on a VPS or shared host, check that nginx or Apache is configured for brotli.
  5. Update the URL. Change <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/..."> to <script src="/vendor/jquery-3.7.1.min.js">. Same for fonts CSS, Bootstrap CSS, etc. Test once. Done.

Per-asset notes

Google Fonts. Use google-webfonts-helper (a free open-source tool, no Google connection) to download just the WOFF2 files for the weights and subsets you actually use. Skip TTF, EOT, and WOFF — modern browsers all support WOFF2. Generate a tiny @font-face declarations CSS file. Total size for a typical 4-weight family is 30-80KB self-hosted versus 130-200KB through Google Fonts (because the gstatic version includes additional charset subsets). Full font self-hosting walkthrough.

jsdelivr / unpkg / cdnjs (npm packages). Pin the exact version in the URL today, then download once with curl: curl -o vendor/foo-1.2.3.min.js https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/foo.min.js. Commit to /vendor/. Generate the SRI hash. Replace the URL.

Bootstrap and other CSS frameworks. Same pattern as the npm packages above. The compiled CSS is a static file. Self-host both the CSS and any JS bundles the framework ships.

Tailwind play CDN (cdn.tailwindcss.com). This is the dev-only runtime that is meant for prototyping. Replace it with a built tailwind.css file: install Tailwind CLI locally (npm install -D tailwindcss), run npx tailwindcss -o public/tailwind.css --minify, and reference the built file. Saves a 350KB+ runtime CSS download because the play CDN ships the entire utility set on every page load.

FontAwesome free. Self-host the woff2 + a tiny CSS sheet. The free icons are a fixed set; FontAwesome's own CDN does not give you anything you cannot get from a downloaded copy.

What you give up

You stop getting "free" version updates from the public CDN. Some teams treat that as a feature — they want to ship a new version of jQuery the moment one releases. In practice you do not want this. Auto-updating a transitive dependency at runtime is a supply-chain risk, exactly the failure mode SRI was invented to prevent. Pin the version. Update on your release schedule. Re-test before deploying.

What the audit does next

The new self-host check in the Mega Analyzer reports the host, the asset URL, and the reason it is flagged. The Mega AI Fix Prompt that the analyzer assembles at the end now includes the per-asset migration steps in its output, so pasting the prompt into Claude or ChatGPT gets you a script-by-script rewrite plan tailored to your stack. Start at /tools/mega-analyzer/ and paste your URL.

Related reading

Fact-check notes and sources

This post is informational, not legal or web-engineering advice. Mentions of specific CDN providers and tools are nominative fair use. No affiliation is implied.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026