Making Cloudflare Docs faster via improving startup times by 316% and page reload times by 344%
Last year, we began investigating alternative frameworks to replace Hugo, the static site generator powering https://developers.cloudflare.com/ since we migrated from Gatsby in 2022.
We quickly decided on Astro and their in-house documentation theme, Starlight. At the time, we had a similar amount of pages as Astro’s own documentation and used them as our comparison for local development and production build performance.
In our previous investigation into our Hugo & Vite setup, a build was around 4 minutes. For Astro, they were building in around 5 and a half minutes. With their commitment to improving performance, such as a ~40% improvement in their own build times shortly before we began our evaluation, we were satisfied that moving away from the native Go tooling to a JavaScript framework would not be impeded by performance.
During our migration, we hit edge cases that we didn’t expect - and for all of them, the edge case was “we have a lot of content.” With a lot of profiling, digging into the code of our dependencies and reporting our findings to upstream projects, we’ve improved our astro dev startup times by 316% and page reload times by 344%.
These improvements aren’t just isolated to our project though - these are performance improvements that make Astro and Vite faster for everyone!
Improving sidebar generation performance in Starlight
As our migration was underway, we noticed that our build times were getting worse per-page with each new page. Taking longer overall for more pages is natural, but we were seeing more like 20ms * 2000 pages and 30ms * 3000 pages.
When we started profiling, we saw generating the sidebar for each page was taking up the majority of the time to render a given page.
We have 4,477 pages in our sidebar, which is filtered into a per-product view. When you are on the Cloudflare One documentation, you will not see pages for the WAF product and vice versa.
With Hugo, the sidebar was also a source of slowdowns. In our original Hugo & Vite performance investigation, sidebar.nav.html and sidebar.navitem.html were amongst our slowest templates.
We opted to patch the Starlight package, improving our build times by 243% with just 5 lines, but this wasn’t a permanent fix. We took our findings, flamegraphs and code we identified as the culprit to the Astro team and they quickly implemented intermediate sidebar caching which benefits all Starlight users.
Our findings could have been produced from a private project all the same, but they were able to test this independently as our repository is open-source.
Optimising Tailwind configuration for local development startup time
One thing that stood out as being particularly slower than our previous Hugo setup was the startup time for Astro’s local development server. We found that the time between running astro dev and the homepage loading in the browser was around 25 seconds.
With a single flamegraph, we were able to identify the culprit as Tailwind’s regular expressions (shown as the getClassCandidates function in the flamegraph) running across all of our Markdown files trying to find utility classes to include in their CSS output.
They have nullified this problem with Tailwind 4, which was already underway at the time, so we did not report this as it was a known issue. With a one line change to exclude our 5,578 Markdown files, we cut the startup time from 25 seconds to 11 seconds.
That was our new baseline. It was as good as it could be with just configuration tweaks, so where do we go from here?
Tackling race conditions in Astro 5’s Content Layer
Astro 5’s Content Layer was created to tackle the scaling issues of content collections, building MDX 1.9x faster and using 25% less memory.
Despite this, our first experiences with Astro 5 were scaling issues. We had so many pages that when a file was changed during local development, HMR would try to reload the page before Astro had saved our changes to the store backing Content Layer.
The Astro team were quick to address this, implementing atomic writes for the store, as well as improved checks for deciding when writing to the store is required as a result of my follow-up issue regarding duplicate reloads.
Whilst our minimal reproductions were with blank content, the size of our site means we can surface race conditions in places you wouldn’t see whilst developing the functionality. We’ve improved the stability of Astro 5 as a result, and now it’s time for us to upgrade.
Improving local development performance in Astro and Vite
With Astro 4, the time taken for the new page to appear in your browser after you edit the file was around 8.5 seconds. This is because of the aforementioned scaling issues, Astro 4 was processing our ~5,000 files on each change.
When we upgraded to Astro 5, this reduced to 6 seconds - a 41% improvement - but we knew it had more to give. We performed a deep dive into the code behind a page reload and found that source maps for the Content Layer store were taking the majority of the time for a reload.
Inline with the Rollup spec, Astro is returning mappings: ''
for the data store’s virtual module, which explicitly indicates that it does not make sense to generate a source map for this file.
Astro uses a Vite plugin to support import.meta.env which was not checking the type of the source file correctly, meaning that any documentation referencing the string import.meta.env would be transformed by this plugin. We reported this to the Astro team with our findings and as a result, they improved the performance of Astro but also protected against possibly breaking builds.
With this one change, our reloads have gone from 6 seconds to 3.5 seconds. Let’s take a look at how we improved this even further by investigating why Vite is still generating a source map for this file.
During local development, known as SSR in much of Vite’s codebase, source maps are collapsed when the script (in this case, the data store virtual module) is transformed. Our findings were that this represented ~2.75 seconds of the ~3.5 seconds for a page reload.
This sounds correct, but it was not respecting mappings: ''
which Rollup documents and instead always generated a source map. This was quickly identified by Vite’s maintainers and, as of Vite 6.1, our reloads are now around 1.6 seconds.
Both of these changes also apply to the startup time of the development server. Before these two fixes, we had an average startup time of 12.17 seconds which is now 5.8 seconds.
Implementing our own feature requests
Now that we’re fully in the JavaScript ecosystem, we’re equipped to give back not only in bug reports and investigations but also in implementing feature requests.
Whilst we’ve resorted to patching Astro or Starlight in the past, these are only temporary measures and we always upstream our improvements. We wanted to use options like resultsFooterComponent
in the DocSearch widget, but as it is a function it couldn’t be serialised like a primitive option. With the Astro team’s guidance, we implemented support for configuring all of Algolia’s DocSearch options in Starlight’s plugin which solves our need but also for any other project looking at using Starlight.
Serving as a reference for other Astro and Starlight users
We’re not just improving performance for everyone relying on these frameworks, Vite itself has 20 million weekly downloads, but we also embody a lot of best practices for documentation.
Our team’s work, from technical writing to content strategy to information architecture, is noticed by other developers who are looking to build their next documentation site:
People frequently ask in Astro’s community how to replicate what we’re doing in Starlight, and since we’re an open-source project, we can just point them right to the code!
Our commitment to open-source
Whilst our move to Astro has greatly enhanced our ability to give back to the open-source community, it’s by no means the beginning. For years, we have treated content like a product and that has always extended down to the platform.
Even as far back as Gatsby, the platform (known then as cloudflare-docs-engine) has always been open-source and allowed projects like Miniflare to host their own documentation. With Hugo and Starlight, we have shown that this is not only a technically beneficial decision but also an interpersonal one:
After compiling our wishlist of new features to implement, we reaffirmed our commitment to open source. We valued the benefit of open source in both the content and the underlying framework of our documentation site. This commitment goes beyond technical considerations, because it’s a fundamental aspect of our relationship with our community and our philosophy of transparency and collaboration. While the choice of an open source framework to build the site on might not be visible to many visitors, we recognized its significance for our community of developers and contributors.
Building on top of open-source projects gives us the flexibility to make changes where necessary but also contribute those improvements back as a result. We’re well-equipped to tread into experimental APIs, our content is at a size above typical benchmarks and we’ve uncovered code paths that wouldn’t normally be an issue for other projects.
We’re super excited with what we’ve been able to achieve in collaboration with the Astro team so far and, if you have any projects using Astro or Vite yourself, hopefully you have benefitted as a result too!