|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- title: Self-Host Your Static Assets
- url: https://csswizardry.com/2019/05/self-host-your-static-assets/
- hash_url: 6f8793385f7b1ea2511bf614b5287397
-
- <details>
- <summary>Table of Contents</summary>
- <ol>
- <li><a href="#what-am-i-talking-about">What Am I Talking About?</a></li>
- <li><a href="#risk-slowdowns-and-outages">Risk: Slowdowns and Outages</a></li>
- <li><a href="#risk-service-shutdowns">Risk: Service Shutdowns</a></li>
- <li><a href="#risk-security-vulnerabilities">Risk: Security Vulnerabilities</a>
- <ol>
- <li><a href="#mitigation-subresource-integrity">Mitigation: Subresource Integrity</a></li>
- </ol>
- </li>
- <li><a href="#penalty-network-negotiation">Penalty: Network Negotiation</a>
- <ol>
- <li><a href="#mitigation-preconnect">Mitigation: <code class="highlighter-rouge">preconnect</code></a></li>
- </ol>
- </li>
- <li><a href="#penalty-loss-of-prioritisation">Penalty: Loss of Prioritisation</a></li>
- <li><a href="#penalty-caching">Penalty: Caching</a></li>
- <li><a href="#myth-cross-domain-caching">Myth: Cross-Domain Caching</a></li>
- <li><a href="#myth-access-to-a-cdn">Myth: Access to a CDN</a></li>
- <li><a href="#self-host-your-static-assets">Self-Host Your Static Assets</a></li>
- </ol>
- </details>
- <p>One of the quickest wins—and one of the first things I recommend my clients
- do—to make websites faster can at first seem counter-intuitive: you should
- self-host all of your static assets, forgoing others’ CDNs/infrastructure. In
- this short and hopefully very straightforward post, I want to outline the
- disadvantages of hosting your static assets ‘off-site’, and the overwhelming
- benefits of hosting them on your own origin.</p>
- <h2 id="what-am-i-talking-about">What Am I Talking About?</h2>
- <p>It’s not uncommon for developers to link to static assets such as libraries or
- plugins that are hosted at a public/CDN URL. A classic example is jQuery, that
- we might link to like so:</p>
- <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
- </code></pre></div></div>
- <p>There are a number of perceived benefits to doing this, but my aim later in this
- article is to either debunk these claims, or show how other costs vastly
- outweigh them.</p>
- <ul>
- <li><strong>It’s convenient.</strong> It requires very little effort or brainpower to include
- files like this. Copy and paste a line of HTML and you’re done. Easy.</li>
- <li><strong>We get access to a CDN.</strong> <code class="highlighter-rouge">code.jquery.com</code> is served by
- <a href="https://www.stackpath.com/products/cdn/">StackPath</a>, a CDN. By linking to
- assets on this origin, we get CDN-quality delivery, free!</li>
- <li><strong>Users might already have the file cached.</strong> If <code class="highlighter-rouge">website-a.com</code> links to
- <code class="highlighter-rouge">https://code.jquery.com/jquery-3.3.1.slim.min.js</code>, and a user goes from there
- to <code class="highlighter-rouge">website-b.com</code> who also links to
- <code class="highlighter-rouge">https://code.jquery.com/jquery-3.3.1.slim.min.js</code>, then the user will already
- have that file in their cache.</li>
- </ul>
- <h2 id="risk-slowdowns-and-outages">Risk: Slowdowns and Outages</h2>
- <p>I won’t go into too much detail in this post, because I have a <a href="https://csswizardry.com/2017/07/performance-and-resilience-stress-testing-third-parties/">whole
- article</a>
- on the subject of third party resilience and the risks associated with slowdowns
- and outages. Suffice to say, if you have any critical assets served by third
- party providers, and that provider is suffering slowdowns or, heaven forbid,
- outages, it’s pretty bleak news for you. You’re going to suffer, too.</p>
- <p>If you have any render-blocking CSS or synchronous JS hosted on third party
- domains, go and bring it onto your own infrastructure <em>right now</em>. Critical
- assets are far too valuable to leave on someone else’s servers.</p>
- <h2 id="risk-service-shutdowns">Risk: Service Shutdowns</h2>
- <p>A far less common occurrence, but what happens if a provider decides they need
- to shut down the service? This is exactly what <a href="https://rawgit.com">Rawgit</a> did
- in October 2018, yet (at the time of writing) a crude GitHub code search still
- yielded <a href="https://github.com/search?q=rawgit&type=Code">over a million
- references</a> to the now-sunset
- service, and almost 20,000 live sites are still linking to it!</p>
- <figure>
- <img src="/wp-content/uploads/2019/05/big-query-rawgit.jpg" alt=""/>
- <figcaption>Many thanks to <a href="https://twitter.com/paulcalvano">Paul
- Calvano</a> who very kindly <a href="https://bigquery.cloud.google.com/savedquery/226352634162:7c27aa5bac804a6687f58db792c021ee">queried
- the HTTPArchive</a> for me.</figcaption>
- </figure>
- <h2 id="risk-security-vulnerabilities">Risk: Security Vulnerabilities</h2>
- <p>Another thing to take into consideration is the simple question of trust. If
- we’re bringing content from external sources onto our page, we have to hope that
- the assets that arrive are the ones we were expecting them to be, and that
- they’re doing only what we expected them to do.</p>
- <p>Imagine the damage that would be caused if someone managed to take control of
- a provider such as <code class="highlighter-rouge">code.jquery.com</code> and began serving compromised or malicious
- payloads. It doesn’t bear thinking about!</p>
- <h3 id="mitigation-subresource-integrity">Mitigation: Subresource Integrity</h3>
- <p>To the credit of all of the providers referenced so far in this article, they do
- all make use of <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity">Subresource
- Integrity</a>
- (SRI). SRI is a mechanism by which the provider supplies a hash (technically,
- a hash that is then Base64 encoded) of the exact file that you both expect and
- intend to use. The browser can then check that the file you received is indeed
- the one you requested.</p>
- <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
- integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8="
- crossorigin="anonymous"></script>
- </code></pre></div></div>
- <p>Again, if you absolutely must link to an externally hosted static asset, make
- sure it’s SRI-enabled. You can add SRI yourself using <a href="https://www.srihash.org/">this handy
- generator</a>.</p>
- <h2 id="penalty-network-negotiation">Penalty: Network Negotiation</h2>
- <p>One of the biggest and most immediate penalties we pay is the cost of opening
- new TCP connections. Every new origin we need to visit needs a connection
- opening, and that can be very costly: DNS resolution, TCP handshakes, and TLS
- negotiation all add up, and the story gets worse the higher the latency of the
- connection is.</p>
- <p>I’m going to use an example taken straight from Bootstrap’s own <a href="https://getbootstrap.com/docs/4.3/getting-started/introduction/">Getting
- Started</a>. They
- instruct users to include these following four files:</p>
- <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="..." crossorigin="anonymous">
- <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="..." crossorigin="anonymous"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="..." crossorigin="anonymous"></script>
- <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="..." crossorigin="anonymous"></script>
- </code></pre></div></div>
- <p>These four files are hosted across three different origins, so we’re going to
- need to open three TCP connections. How much does that cost?</p>
- <p>Well, on a reasonably fast connection, hosting these static assets off-site is
- 311ms, or 1.65×, slower than hosting them ourselves.</p>
- <figure>
- <img src="/wp-content/uploads/2019/05/wpt-off-site-cable.png" alt=""/>
- <figcaption>By linking to three different origins in order to serve static
- assets, we cumulatively lose a needless 805ms to network negotiation. <a href="https://www.webpagetest.org/result/190531_FY_618f9076491312ef625cf2b1a51167ae/3/details/">Full
- test.</a></figcaption>
- </figure>
- <p>Okay, so not exactly terrifying, but Trainline, a client of mine, found that by
- reducing latency by 300ms, <a href="https://wpostats.com/2016/05/04/trainline-spending.html">customers spent an extra £8m
- a year</a>. This is
- a pretty quick way to make eight mill.</p>
- <figure>
- <img src="/wp-content/uploads/2019/05/wpt-self-hosted-cable.png" alt=""/>
- <figcaption>By simply moving our assets onto the host domain, we completely
- remove any extra connection overhead. <a href="https://www.webpagetest.org/result/190531_FX_f7d7b8ae511b02aabc7fa0bbef0e37bc/3/details/">Full
- test.</a></figcaption>
- </figure>
- <p>On a slower, higher-latency connection, the story is much, much worse. Over 3G,
- the externally-hosted version comes in at an eye-watering <strong>1.765s slower</strong>.
- I thought this was meant to make our site faster?!</p>
- <figure>
- <img src="/wp-content/uploads/2019/05/wpt-off-site-3g.png" alt=""/>
- <figcaption>On a high latency connection, network overhead totals a whopping
- 5.037s. All completely avoidable. <a href="https://www.webpagetest.org/result/190531_XE_a95eebddd2346f8bb572cecf4a8dae68/3/details/">Full
- test.</a></figcaption>
- </figure>
- <p>Moving the assets onto our own infrastructure brings load times down from around
- 5.4s to just 3.6s.</p>
- <figure>
- <img src="/wp-content/uploads/2019/05/wpt-self-hosted-3g.png" alt=""/>
- <figcaption>By self-hosting our static assets, we don’t need to open any more
- connections. <a href="https://www.webpagetest.org/result/190531_ZF_4d76740567ec1eba1e6ec67acfd57627/1/details/">Full
- test.</a></figcaption>
- </figure>
- <p>If this isn’t already a compelling enough reason to self-host your static
- assets, I’m not sure what is!</p>
- <h3 id="mitigation-preconnect">Mitigation: <code class="highlighter-rouge">preconnect</code></h3>
- <p>Naturally, my whole point here is that you should not host any static assets
- off-site if you’re otherwise able to self-host them. However, if your hands are
- somehow tied, then you can use <a href="https://speakerdeck.com/csswizardry/more-than-you-ever-wanted-to-know-about-resource-hints?slide=28">a <code class="highlighter-rouge">preconnect</code> Resource
- Hint</a>
- to preemptively open a TCP connection to the specified origin(s):</p>
- <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><head>
-
- ...
-
- <link rel="preconnect" href="https://code.jquery.com" />
-
- ...
-
- </head>
- </code></pre></div></div>
- <p>For bonus points, deploying these as <a href="https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/">HTTP
- headers</a>
- will be even faster.</p>
- <p><strong>N.B.</strong> Even if you do implement <code class="highlighter-rouge">preconnect</code>, you’re still only going to make
- a small dent in your lost time: you still need to open the relevant connections,
- and, especially on high latency connections, it’s unlikely that you’re ever
- going to fully pay off the overhead upfront.</p>
- <h2 id="penalty-loss-of-prioritisation">Penalty: Loss of Prioritisation</h2>
- <p>The second penalty comes in the form of a protocol-level optimisation that we
- miss out on the moment we split content across domains. If you’re running over
- HTTP/2—which, by now, you should be—you get access to prioritisation. All
- streams (ergo, resources) within the same TCP connection carry a priority, and
- the browser and server work in tandem to build a dependency tree of all of these
- prioritised streams so that we can return critical assets sooner, and perhaps
- delay the delivery of less important ones.</p>
- <p><small><strong>N.B.</strong> Technically, owing to H/2’s <a href="https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/">connection
- coalescence</a>,
- requests can be prioritised against each other over different domains as long as
- they share the same IP address.</small></p>
- <p>If we split our assets across multiple domains, we have to open up several
- unique TCP connections. We cannot cross-reference any of the priorities within
- these connections, so we lose the ability to deliver assets in a considered and
- well designed manner.</p>
- <p>Compare the two HTTP/2 dependency trees for both the off-site and self-hosted
- versions respectively:</p>
- <figure>
- <img src="/wp-content/uploads/2019/05/wpt-dep-tree-off-site.png" alt=""/>
- <figcaption>Notice how we need to build new dependency trees per
- origin? Stream IDs 1 and 3 keep reoccurring.</figcaption>
- </figure>
- <figure>
- <img src="/wp-content/uploads/2019/05/wpt-dep-tree-self-hosted.png" alt=""/>
- <figcaption>By hosting all content under the same origin, we can build one, more
- complete dependency tree. Every stream has a unique ID as they’re all in the
- same tree.</figcaption>
- </figure>
- <p><small>Fun fact: Stream IDs with an odd number were initiated by the client;
- those with an even number were initiated by the server. I honestly don’t think
- I’ve ever seen an even-numbered ID in the wild.</small></p>
- <p>If we serve as much content as possible from one domain, we can let H/2 do its
- thing and prioritise assets more completely in the hopes of better-timed
- responses.</p>
- <h2 id="penalty-caching">Penalty: Caching</h2>
- <p>By and large, static asset hosts seem to do pretty well at establishing
- long-lived <code class="highlighter-rouge">max-age</code> directives. This makes sense, as static assets at versioned
- URLs (as above) will never change. This makes it very safe and sensible to
- enforce a reasonably aggressive cache policy.</p>
- <p>That said, this isn’t always the case, and by self-hosting your assets you can
- design <a href="https://csswizardry.com/2019/03/cache-control-for-civilians/">much more bespoke caching
- strategies</a>.</p>
- <h2 id="myth-cross-domain-caching">Myth: Cross-Domain Caching</h2>
- <p>A more interesting take is the power of cross-domain caching of assets. That is
- to say, if lots and lots of sites link to the same CDN-hosted version of, say,
- jQuery, then surely users are likely to already have that exact file on their
- machine already? Kinda like peer-to-peer resource sharing. This is one of the
- most common arguments I hear in favour of using a third-party static asset
- provider.</p>
- <p>Unfortunately, there seems to be no published evidence that backs up these
- claims: there is nothing to suggest that this is indeed the case. Conversely,
- <a href="https://discuss.httparchive.org/t/analyzing-resource-age-by-content-type/1659">recent
- research</a>
- by <a href="https://twitter.com/paulcalvano">Paul Calvano</a> hints that the opposite might
- be the case:</p>
- <blockquote>
- <p>There is a significant gap in the 1st vs 3rd party resource age of CSS and web
- fonts. 95% of first party fonts are older than 1 week compared to 50% of 3rd
- party fonts which are less than 1 week old! This makes a strong case for self
- hosting web fonts!</p>
- </blockquote>
- <p>In general, third party content seems to be less-well cached than first party
- content.</p>
- <p>Even more importantly, <a href="https://andydavies.me/blog/2018/09/06/safari-caching-and-3rd-party-resources/">Safari has completely disabled this
- feature</a>
- for fear of abuse where privacy is concerned, so the shared cache technique
- cannot work for, at the time of writing, <a href="http://gs.statcounter.com/">16% of users
- worldwide</a>.</p>
- <p>In short, although nice in theory, there is no evidence that cross-domain
- caching is in any way effective.</p>
- <h2 id="myth-access-to-a-cdn">Myth: Access to a CDN</h2>
- <p>Another commonly touted benefit of using a static asset provider is that they’re
- likely to be running beefy infrastructure with CDN capabilities: globally
- distributed, scalable, low-latency, high availability.</p>
- <p>While this is absolutely true, if you care about performance, you should be
- running your own content from a CDN already. With the price of modern hosting
- solutions being what they are (this site is fronted by Cloudflare which is
- free), there’s very little excuse for not serving your own assets from one.</p>
- <p>Put another way: if you think you need a CDN for your jQuery, you’ll need a CDN
- for everything. Go and get one.</p>
- <h2 id="self-host-your-static-assets">Self-Host Your Static Assets</h2>
- <p>There really is very little reason to leave your static assets on anyone else’s
- infrastructure. The perceived benefits are often a myth, and even if they
- weren’t, the trade-offs simply aren’t worth it. Loading assets from multiple
- origins is demonstrably slower. Take ten minutes over the next few days to audit
- your projects, and fetch any off-site static assets under your own control.</p>
|