Our goals for the new website were manifold:
Since we are huge fans of Ember.js and are heavily connected with the community, supporting it and even directly involved in its core team, we wanted to stay in the ecosystem to build our own site as well. While Ember.js is a great fit for ambitious apps like travel booking systems or appointment scheduling systems that implement significant client side logic though, for a static page like ours it would admittedly not have been an ideal fit – we would simply not have needed or used much of what it comes with. Ember.js' lightweight sister project Glimmer.js provides exactly what we need though, which is a system for defining and rendering components and trees of components that would get re-rendered upon changes to the application state.
Statically pre-rendering a client-side app at build time is relatively straight forward. As part of our Netlify deployment, we build the app and start a small Express Server that serves it. We then visit each of the routes with a headless instance of Chrome using Puppeteer, take a snapshot of the page's DOM and save that to a respectively named file. All of these HTML files, along with the app itself and all other assets, then get uploaded to the CDN to be served from there.
Although we were switching to a significantly more advanced setup than what we had with the previous Jekyll-based site, we did not want to give up the easy maintenance of content, specifically for blog posts and similar content that we wanted to keep in Markdown files as we used to. Writing a new post should remain as easy as adding a new markdown file with some front matter and Markdown-formatted content. At the same time, we did not want to rely on an API for loading the content of particular pages dynamically as that would have added significant additional complexity and none of our data actually needed to be computed on demand on the server as all of it is indeed static and known upfront. Leveraging Glimmer.js' Broccoli-based build pipeline, we set up a process that reads in all files in a directory and converts the Markdown files into Glimmer.js components at build time.
That way we are generating dedicated components for all posts that are all mapped to their own routes. We also generate the components for the blog listing page(s) and the ones that list all posts by a particular author. This approach basically moves what would typically be done by an API server at runtime (retrieving content from a repository that grows and changes over time) to build time, much like what the Jamstack advocates for (and tools like Empress, VuePress or Gatsby would have done out of the box 😀). The same approach is used for other parts of the website that grow and change over time and that we want to be able to maintain content for with little effort like the calendar or talks catalog.
Another factor to take into account when defining bundle boundaries is the stability of each bundle in the sense of how often it is going to change over time. Our main bundle that contains Glimmer.js and the site's main content is relatively stable and will typically not change for longer periods of time (potentially weeks or months). That means once it is cached in a user's browser, there is a good chance they will be able to reuse it from cache upon their next visit. If we had included all of the components for all of the blog posts in that main bundle though, we would not only have steadily grown that bundle over time but also invalidated the users's cache for it every time we released a new post. The same is true for the component that renders a list of recent posts for a particular topic. As that component is always needed along with components that are part of the main bundle as it is rendered on the respective pages, we could have included it right with the main bundle, but that would likewise have meant invalidating the main bundle with every blog post which would have resulted in a poor utilisation of our user's caches.
As described, we optimized our bundles for cache-ability. Since we also use fingerprinted asset names (or actually get them for free out of the box since Glimmer.js uses Ember CLI), we can let our user's browsers cache all resources indefinitely using immutable caching:
immutable caching directive tells the browser that the respective resource
can never change and may be cached indefinitely. The
max-age directive is only
necessary as a fallback for browsers that
do not support immutable caching.
An immutable resource that the browser has cached will be available instantly on
the next visit to the respective page and should generally have the same
performance characteristics as a resource cached in a service worker's cache.
<body> that the application will render into once it starts up.
All of the above has lead to a result we are pretty happy with. While the design of our new page is for everyone to judge based on their own taste maybe, the performance numbers speak a clear language.
We were able to get there without giving up on the ease of maintenance of the content so that writing a new blog post is as easy as adding a Markdown file and opening a pull request.
And even though we spent an unreasonable amount of time and effort during the course of the project, there are many more things that we did not do or that I couldn't cover in this article but that should be considered best practices when optimizing for performance:
By spending a significant (and maybe unreasonable) amount of time and energy we ended up with the highly optimized site you're looking at. The downside is we ended up with our own custom static site generator essentially that we now need to maintain ourselves (one of the reasons why we recommend using a fully integrated framework like Ember.js instead of compiling your own custom framework out of a bunch of micro libraries). However, it was definitely an interesting experiment and we hope you take some inspiration out of the patterns and mechanisms we describe in this post. If you are struggling with performance in your Ember.js or Glimmer.js or other apps, feel free to reach out and talk to our experts to see how we can help.