Engineering Journal
Table Formatter
Table Formatter

If your app cannot display its content without a CDN, it does not work offline

2026-08-21

TLDR: Split third-party scripts into two classes. Chrome dependencies degrade the UI when they fail; content dependencies make the content itself unreadable. Bundle the second class even when you happily CDN the first.

The position

"Load libraries from a CDN" is good default advice and it hides a distinction that matters more than the bytes.

Some libraries make your interface work. Lose the animation library and buttons stop easing: annoying, survivable, and the user still gets what they came for.

Other libraries turn stored data into something a human can read. Lose those and the user is staring at source. The content is present, intact, correctly stored, and unreadable.

Treating these as one category is how an app that claims to work offline stops working offline for exactly the documents that most need it to.

What the industry does and why

The standard advice is sound on its own terms: geographic distribution, caching, a smaller bundle. For a static tool with no build step, a script tag pointing at a CDN is the entire integration story.

The cache-sharing argument that used to justify it is dead, incidentally. Browsers partitioned their HTTP caches by origin years ago, so a visitor loading the same jQuery on two sites downloads it twice. What remains is bandwidth you do not serve and a build step you do not need. For chrome, that trade is still fine: the failure mode is cosmetic and temporary.

Why it fails for this problem class

Our tool is a table and document editor whose central claim is that your files never leave your device. It loads jQuery, Bootstrap and GSAP from a CDN, and I am not proposing changing that. Lose them and you get an ugly page that still edits tables.

Then we added equation editing, and the shape of the risk changed. The rendered equation is the thing being edited. Someone correcting the maths a PDF extraction got wrong is doing exactly one thing: comparing what they typed against what it renders as.

Take the renderer away and the surface has no purpose. Not degraded, not slower: pointless. You would be typing backslashes into a text box with no way to tell whether you are right. "Everything happens in your browser" also cannot be true if the browser must reach a third-party host before it can display what you extracted.

So we vendored it. A script copies the renderer, its stylesheet and its woff2 fonts out of the package into the tool, and is committed so the copy cannot silently drift from the version the sibling tool bundles:

// The stylesheet references woff2, woff and ttf. Only woff2 is copied (every
// browser that can run this tool has supported it since 2016), so the fallback
// src entries are stripped rather than left dangling.
css = css.replace(/,url\(fonts\/[^)]+\.(?:woff|ttf)\)\s*format\("[^"]+"\)/g, '');

The check at the end matters more than the copy. It walks the stylesheet's font references and fails if any file is absent, because a missing font does not error: it renders in a fallback face at the wrong metrics, and the equation is subtly wrong in a way nobody reports.

What you give up

Bundle size. 640 KB, most of it fonts. Real, and it buys the ability to display content at all.

A manual version bump. The script has to be re-run when the dependency moves. That is the honest cost of pinning: you now know which version you ship, which you did not before.

The tidiness of one rule. "CDN everything" is easier to hold in your head than "CDN chrome, bundle content." The second needs a judgement per library, though the judgement is small: if this fails to load, can the user still read their own data?

When the common pattern is right

Almost always, for almost everything. CDN your UI framework, your icon set, your animation library, your analytics. Their failure mode is a worse-looking page, and a worse-looking page that still works is a fine outcome for a rare event.

The exception is narrow: the library that renders the user's content, in an app whose value is working without a network. For a document viewer that is the layout engine. For a diagramming tool, the diagram renderer. For us, the maths.

If you cannot name a content dependency in your app, you probably do not have one and the default advice is right. If you can name it, ask what your product promises when the network is gone, and whether that promise survives.

Read this post in the full Engineering Journal →