How to use CDNs properly

Compare CDNsCommon misconceptions about CDNs


Use HTTPS-Only URLs

Secure connections rely on all the content in a page being loaded over another secure connection. You can use protocol relative URLs, which will copy the protocol of the parent, or specify for all requests to be secure.

Throws a security warning on secure pages.

src="http://cdn.example.com/library.js"

Fails when running over a local file connection.

src="//cdn.example.com/library.js"

Always negotiates a secure connection.

src="https://cdn.example.com/library.js"

Use Subresource Integrity (SRI)

When you load a script or stylesheet from a third-party CDN, you are trusting that CDN to serve exactly the code you expect. Subresource Integrity lets the browser verify that the file hasn't been tampered with by checking it against a cryptographic hash you provide.

If the CDN is compromised or the file is modified in transit, the browser will refuse to execute it. This is your most important defence against CDN supply chain attacks.

No integrity check. Trusts the CDN blindly.

<script src="https://cdn.example.com/library.js"> </script>

Verified with SRI. Rejects tampered files.

<script src="https://cdn.example.com/library.js" integrity="sha384-..." crossorigin="anonymous"> </script>

Generate SRI hashes with openssl dgst -sha384 -binary file.js | openssl base64 -A, or use srihash.org. Both jsDelivr and CDNJS provide SRI hashes on their websites.

Always include the crossorigin="anonymous" attribute alongside integrity. Without it, the browser may skip the integrity check for cross-origin resources.


Use Resource Hints

Before the browser can download a file from a CDN, it needs to resolve DNS, establish a TCP connection, and negotiate TLS. Resource hints let you start this process early, before the browser discovers the resource naturally.

Resolve DNS and establish connection early.

<link rel="preconnect" href="https://cdn.example.com"> <link rel="dns-prefetch" href="https://cdn.example.com">

Start downloading a critical resource immediately.

<link rel="preload" href="https://cdn.example.com/library.js" as="script">

Use preconnect when you know you'll need resources from a CDN but don't know the exact URLs yet. Use preload when you know the exact file. Include dns-prefetch as a fallback for browsers that don't support preconnect.


Load JavaScript Asynchronously

JavaScript not only has to be loaded before the page continues loading, but typically has to be executed before continuing as well. This can create massive waits, especially in pages with multiple libraries placed in the header. While a favorite recommendation has always been "move scripts to the footer", this means any external resources do not start downloading in parallel with the page.

Google Analytics, among others, solves this problem by placing a small inline script that then creates a script element that is downloaded without blocking the page. The small inline script itself does, of course, but is generally of little consequence.

While this continues to be used for single, stand-alone scripts, i.e. those with no dependencies, it does not function well with the larger libraries you page may rely on. For instance, using either the async or defer properties in conjunction with your jQuery will cause an error if later scripts run before it is finished loading. These errors look something like $ is not defined or jquery is not defined.

It has historically been very difficult to start loading jQuery early,
without blocking the page, and while preserving and other scripts you encounter.

The issue arises from the fact that any inline or external scripts are run once loaded, whereas async or defer are only run once loaded or the whole page is ready, and there is no guarantee with any use of these that they will be run in order.

What we need then, with jQuery especially, is a way to start loading it early on, in the head preferably, and then "catch" any scripts that try to execute using jQuery functions, and hold them until it is ready. Then it should execute them in order, as it otherwise would have.

Doesn't work at all.

<script> $('footer').css('background', 'black'); </script> </head> ... <script src="library.js"></script> </body>

Blocks rest of page from loading.

<script src="library.js"></script> <script> $('footer').css('background', 'black'); </script> </head>

Doesn't load until the page is done.

<script src="library.js"></script> <script> $('footer').css('background', 'black'); </script> </body>

Use ES Modules — deferred and ordered by default.

If you don't need to support legacy browsers, native ES modules provide a built-in solution to the dependency ordering problem. Modules are deferred by default and respect their import graph, so dependencies are always resolved in the correct order without any loader library.

<script type="module"> import $ from 'https://esm.sh/jquery@3'; $('footer').css('background', 'black'); </script>

For more complex dependency trees, consider import maps, which let you map bare module specifiers to CDN URLs without a bundler.


Make Fewer Requests

Note: With HTTP/2 and HTTP/3 now universally supported by CDNs and modern browsers, the performance benefit of combining files is much smaller than it once was. Consider whether combining is worth the added complexity and reduced cache granularity.

jsDelivr provides a file combiner that can bundle multiple libraries into a single request. This was a significant optimisation under HTTP/1.1, and can still help when loading many very small files.

One Request for Every Library.

<script src="/js/library-a.js"></script> <script src="/js/library-b.js"></script> <script src="/js/library-c.js"></script>

One Request for Multiple Libraries.

<script src="https://cdn.example.com/combine/a.js,b.js,c.js"> </script>

Have a Lower Latency

Check CDNperf for current latency rankings. Cloudflare (which backs CDNJS) and jsDelivr consistently perform well globally. Latency is the time it takes for a user's browser to reach a CDN, so lower latency means faster first bytes.

Regional performance varies. If your users are concentrated in specific regions, test from those locations rather than relying on global averages.


Have a Fallback Strategy

CDNs can go down, get blocked, or be unreachable from certain networks. Always have a plan for when a CDN resource fails to load.

Classic inline fallback.

<script src="https://cdn.example.com/library.js" integrity="sha384-..." crossorigin="anonymous"> </script> <script> window.Library || document.write( '<script src="/js/library.js"><\/script>' ); </script>

For CSS, fallbacks are harder. Consider inlining critical CSS and loading CDN stylesheets with a JavaScript-based loader that can detect failures, or simply self-host stylesheets that are essential to your layout.

ES Module fallbacks with import maps.

For ES modules, import maps let you define fallback URLs declaratively. While browser support for multiple fallback URLs is still evolving, you can use a service worker or a small loader script to handle module fallbacks.

<script type="importmap"> { "imports": { "library": "https://cdn.example.com/library.js" } } </script> <script type="module"> import lib from 'library'; </script>

A well-designed site should degrade gracefully. If your page is unusable without JavaScript, that's a bigger problem than CDN availability.


Set a Content Security Policy

A Content Security Policy (CSP) header tells the browser which origins are allowed to serve scripts, styles, and other resources for your page. When using CDNs, you need to explicitly allowlist each CDN domain.

Example CSP header allowing a CDN.

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' https://cdn.example.com;

CSP works well alongside SRI. With 'strict-dynamic' in your script-src, the browser will trust any script that passes its SRI check, regardless of origin. This can simplify your policy when using multiple CDNs.

Start with Content-Security-Policy-Report-Only to log violations without breaking your site, then switch to enforcing once you've resolved any issues.