Why We Switched CDNs: How Google's Core Web Vitals Led Us to Cloudflare Pages

Michael

As a quick refresher, the Core Web Vitals are a set of metrics developed by Google used to measure real-world user experience. They define an assessment where a website must score 'Good' in all three categories in the 75th percentile to pass. The data used in the assessment is collected from real world users. The metric thresholds are as follows:

'Good' Threshold
Largest Contentful Paint≤2500ms
First Input Delay≤100ms
Cumulative Layout Shift≤0.1

You can measure your own websites Core Web Vitals using PageSpeed Insights, or if you're hosted on Cloudflare Pages, turn on Web Analytics and you'll get an excellent dashboard of this information.

Our website, at the time hosted on Netlify, recently had an influx of traffic from mobile users with sub-optimal network conditions. Where we used to pass Google's "Core Web Vitals Assessment" with great figures - this new user base resulted in our Time to First Byte figures exceeding 1.9s in the p75 interval. Pushing back the first response also pushes back the First and Largest Contentful Paints. Our Largest Contentful Paint figure became 3.7s in the p75 interval for this group of users.

When the network requires 1.9s to push the first byte to the user, it's very difficult to achieve the Largest Contentful Paint within the 2.5s benchmark required to pass the Core Web Vitals Assessment.

It's impossible for us to get HAR archives from these edge users, so we found we couldn't replicate our problems for Netlify. Trying another CDN to compare was our only option.

We have substantial in-house build infrastructure, which is much faster than any affordable cloud-based build solution. Gatsby is our static site generator, and we have custom plugins that handle long-lived build caches for expensive operations like video transcoding. As such we're only looking for web hosting, not an all-in-one build-and-host solution.

Considering Gatsby Cloud

When I originally contacted Gatsby Concierge, requesting some insight into our Core Web Vitals woes, they sent back:

Our Gatsby support organization is set up to support Gatsby Cloud. So we're not able to troubleshoot issues with Gatsby when you're running your site on Netlify. Without question, the best experience for your Gatsby site will be on Gatsby Cloud.

The marketing for this concierge service doesn't mention that anywhere. I wanted a paid support option for the OSS project I've worked with and contributed to for years. It's a bit of a shame they're using it primarily as a marketing funnel to Gatsby Cloud.

Gatsby Cloud forces you to build your site within their infrastructure, which for us would be slow and expensive. We would need to put in additional development effort to get our persistent caches to work on unfamiliar hardware outside of our network. There are many unknowns regarding if our FFMPEG plugin would even run in any reasonable time on their infrastructure.

Netlify, Vercel, and Cloudflare Pages all allow you to upload pre-built artifacts, which is our preferred way of dealing with a web host. Gatsby Cloud is also unique in artificially limiting you to 5000 pages on the $50 a month plan.

The final nail in the coffin was that they only have CDN points of presence in the US and the EU at any price point other than "contact us". Even their homepage seems to exclusively use points of presence in the US and the EU. I think there's a disconnect between their marketing and what they're offering to their average user. Looking through their showcased sites, the ones hosted on Gatsby Cloud only have points of presence in the EU and the US. Purchasing CDN services directly from Fastly (their upstream provider) would be the same price for our use case, with fewer limitations and more points of presence.

Measuring the TTFB stats for the Gatsby homepage (presumably hosted on Gatsby Cloud), while EU and US performance is comparable to Netlify and Cloudflare - Asia Pacific and Africa all had hot cache hits taking 400ms+. Even Sydney, Australia was doing round trips to Los Angeles for a TTFB time of 581ms. These numbers are much worse than all the other offerings. I don't know why a global CDN isn't standard, even if their builds only take place in 'preferred' data centers, especially since it's an explicitly stated feature of their hosting service.

Considering Vercel

Vercel looked very reasonable. Their Web Vitals Analytics only has a 7 day reporting window for the Pro plan which has a minimum cost of $20 per month. Their TTFB stats were very favorable.

TTFB Benchmark results

The methodology of this test is pretty crude, but it serves as an indicative result. Cloudflare and Netlify both served our production site on our production domain, whereas the Gatsby and Vercel figures are just of their respective homepages. Given this is a server TTFB test, the exact content served shouldn't matter as long as the caches are hot. The speedvitals TTFB test was run twice per CDN to warm up the cache, then the third result was used. Cache hits were validated by checking the x-cache header.

LocationCloudflareVercelNetlifyGatsby CloudBest
London, UK158 ms236 ms65 ms86 msNetlify by 21ms
Paris, France81 ms218 ms291 ms47 msGatsby Cloud by 34ms
Sweden172 ms252 ms125 ms220 msNetlify by 47ms
Finland176 ms251 ms151 ms49 msGatsby Cloud by 102ms
Belgium102 ms211 ms65 ms377 msNetlify by 37ms
Madrid, Spain132 ms250 ms117 ms127 msNetlify by 10ms
Milan, Italy130 ms48 ms484 ms87 msVercel by 39ms
Netherlands158 ms207 ms52 ms81 msNetlify by 29ms
Warsaw, Poland77 ms357 ms95 ms165 msCloudflare by 18ms
Frankfurt, Germany97 ms235 ms35 ms42 msNetlify by 7ms
Zurich, Switzerland205 ms178 ms379 ms92 msGatsby Cloud by 86ms
Las Vegas, US73 ms187 ms572 ms187 msCloudflare by 114ms
Los Angeles, US83 ms205 ms46 ms132 msNetlify by 37ms
Iowa, US106 ms200 ms121 ms77 msGatsby Cloud by 29ms
South Carolina, US124 ms222 ms357 ms128 msCloudflare by 4ms
Northern Virginia, US81 ms310 ms34 ms155 msNetlify by 47ms
Oregon, US292 ms55 ms213 ms156 msVercel by 101ms
Dallas, US81 ms95 ms123 ms158 msCloudflare by 14ms
Montreal, Canada94 ms255 ms69 ms151 msNetlify by 25ms
Toronto, Canada101 ms78 ms83 ms94 msVercel by 5ms
São Paulo, Brazil111 ms152 ms31 ms697 msNetlify by 80ms
Santiago, Chile85 ms755 ms224 ms478 msCloudflare by 139ms
Mumbai, India152 ms218 ms1200 ms953 msCloudflare by 66ms
Delhi, India192 ms95 ms262 ms1200 msVercel by 97ms
Taiwan117 ms63 ms188 ms543 msVercel by 54ms
Hong Kong70 ms100 ms159 ms746 msCloudflare by 30ms
Tokyo, Japan86 ms43 ms245 ms428 msVercel by 43ms
Osaka, Japan70 ms29 ms269 ms486 msVercel by 41ms
Seoul, South Korea443 ms27 ms619 ms618 msVercel by 416ms
Singapore489 ms173 ms27 ms686 msNetlify by 146ms
Jakarta, Indonesia508 ms244 ms72 ms787 msNetlify by 172ms
Sydney, Australia97 ms160 ms31 ms581 msNetlify by 66ms
Melbourne, Australia342 ms197 ms79 ms688 msNetlify by 118ms
Tel Aviv, Israel313 ms501 ms411 ms385 msCloudflare by 72ms
South Africa244 ms336 ms1200 ms1600 msCloudflare by 92ms
Average167 ms204 ms243 ms385 msCloudflare by 37ms
StdDev120 ms140 ms286 ms367 msCloudflare by 20ms

Overall Cloudflare was the winner with both a faster average TTFB and a tighter standard deviation.

If you're interested in the 'excel formula' to calculate the 'best' column, it is this monstrosity:

XLOOKUP(0,B2:E2,B$1:E$1,"None",1,1)&" by "& XLOOKUP(MIN(B2:E2)+0.5,B2:E2,B2:E2,0,1,1) − MIN(B2:E2)& "ms"

Cloudflare Pages

Cloudflare ran their own series of benchmarks comparing the CDN performance of Akamai, Cloudflare, Amazon CloudFront, Fastly, and Google. They came out on top there too! We already use them as our DNS host, so trying out their static site host option, Cloudflare Pages seemed like a no-brainer.

Setting up Wrangler (their CLI tool) was simple. All we needed to do was tell Wrangler to upload our built site artifacts. Upon pushing our site to a test deployment, we discovered in-built redirect rules that conflicted with our desired site configuration.

I prefer URLs with no trailing slash, such as https://electricui.com/features. Gatsby produces folders with an index.html file for each of our pages.

Netlify would present the file /features/index.html to both /features/ and /features without redirects when the "Pretty Urls" option was switched off. We could then state /features as our canonical URL, and use JS to rewrite the location transparently without having to refetch anything from the network under any potential URL hit. This gave the best of all worlds in terms of SEO and load performance.

Cloudflare Pages has no option to disable redirects or to specify which 'flavor' is desired. The file /features/index.html is presented primarily at /features/ with a 308 Permanent Redirect from /features to /features/.

This was undesirable. Our canonical URLs are all at the non-trailing-slash version of these URLs. While we could rewrite the URL on the client to 'look nice', every link back would trigger a redirect, increasing the time to load the page, going backward on our desire to reduce TTFB and Largest Contentful Paint metrics.

Vercel's hosting explicitly supports a trailingSlash setting, where all this can be configured explicitly, which is neat.

How to disable the trailing slash on Cloudflare Pages

With Cloudflare solidified as our choice of host, how can we get our desired canonical URLs?

Cloudflare Pages has the following redirect and serve behavior:

URL/file/file//file.html/folder/folder//folder/index.html
behaviorserves file.htmlredirect to /fileredirect to /fileredirect to /folder/serves folder/index.htmlredirect to /folder/

It is interesting to note that Cloudflare uses 308 Permanent Redirects instead of 301 Moved Permanently redirects.

Fortunately, that table of behavior does give us the ability to get the canonical URLs we want without redirects. Instead of structuring our html output as /folder/index.html, we can move the index files up a level and rename them to the name of the folder they were in. Instead of /features/index.html -> /features.html.

Gatsby has explicitly decided not to support 'page unfurling', which is the name they give to this transformation. Gatsby Cloud however can handle your trailing slash option of choice, but at the CDN side.

In any case, Linux to the rescue! By running the following in the Gatsby output directory, every index.html file found (that isn't the root index.html) will be moved up a level and renamed to match the folder it was just within. It works recursively, and won't delete any folders (even if they're empty after this operation has been completed).

find . -name 'index.html' -mindepth 2 -type f -exec sh -c '
parent="$(dirname "$1")";
mv "$1" "$parent/../$(basename "$parent").html";
' find-sh {}

It takes the output folder which looks like this:

/index.html
/pricing/index.html
/features/index.html
/blog/index.html
/blog/fast-react-text-updates/index.html

And turns it into this:

/index.html
/pricing.html
/features.html
/blog.html
/blog/fast-react-text-updates.html

The resulting redirect behavior by Cloudflare pages is /features/ presents a 308 Permanent Redirect to /features and /features presents our features page at our canonical URL. This is probably better for SEO than our previous "200 for every response" behavior, since some search engine scraper might not respect the canonical URL information.

Getting the preview URL out of wrangler

As an aside, the following grep command was used to extract the preview URL from the non-production uploads:

wrangler pages publish ./public --project-name staging | grep -oE "(https:\/\/.+)\b" > ./deploy-url.txt

It's a simple grep, but if you've found this web page searching for "How to disable the trailing slash on Cloudflare Pages", getting this information into your CI solution is probably on your list of tasks.

Core Web Vitals

Given the Core Web Vitals Assessment is what started all this, imagine my delight when Cloudflare pages presented me with frequently updated, time sliceable and filterable Core Web Vitals analytics for every page!

We can trivially filter for mobile and see 90% of our users are having a good experience.

Core Web Vitals in Cloudflare Web Analytics

We can pick a time period, percentiles and view outliers; some poor person took 30 seconds to load the features page background video!

LCP Data in Cloudflare Web Analytics

The debug view is excellent, showing exactly which element took the most time to render. On our features page, it tends to be either the poster or the video itself for the hero element, which is to be expected. It is unfortunate that some users take 19 seconds to load a 16kb image!

LCP Debug View in Cloudflare Web Analytics

Overall I have been very happy with the switch. The performance has been a significant improvement. The analytics has more detail and filterability for free than Netlify's $9 a month offering. Core Web Vitals information as a first class analytics citizen was a welcome surprise, as far as I know Vercel is the only other platform to support this out of the box. I like that I can explicitly delete deployments, whereas Netlify insists on everything being on the web forever. Having unlimited bandwidth and requests also gives peace of mind. The free price tag is pretty difficult to beat.

Hopefully our Core Web Vitals Assessment updates soon to reflect the change's positive impact.

As an interesting note, the trailing slash on the root (https://electricui.com/) is required by the spec - however browsers tend to hide it! You can read about it and every other piece of the URI scheme in IETF RFC 3986 Section 3.