Our little content site etch.co was built with Next.js until we recently migrated it to Eleventy. Even though no features were changed or removed the statistics show a dramatically different result:
| Lighthouse performance score | 76 | 97 |
| Homepage Kilobytes of JavaScript | 2,161KB | 11.3KB |
| Homepage JavaScript file count | 33 | 2 |
| NPM dependency count | 1115 | 13 |
| Overall lines of code | 6,877 | 4,307 |
Use the browser
The numbers clearly don’t favour Next.js, but this isn’t a story about frameworks, quite the opposite.
Eleventy (11ty) doesn’t have some special performance enhancing feature or a magic syntax which massively reduces lines of code.
Instead, it provides the minimalistic set of tools to template some HTML, bundle some CSS and lastly sprinkle JavaScript in the few places it’s needed. Then, it gets completely out the way and lets the browser do the rest.
This leads to, what in our opinion, are 11ty’s biggest strengths:
- Clear separation between build and browser 11ty is build-time-only, we can trust it’s not going to ship extra code to the browser after the build.
- Stability In busy periods where we’re focused on our clients we don’t want to spend what time we do have on our site fixing incoming breaking changes from dependencies. 11ty has only had 3 major version changes since 2018.
We’ve found 11ty’s lightweight feature set ideal for our needs, but it doesn’t always lead to the best developer experience. In some cases, more work was needed up front to achieve parity with Next.js features that are either provided out of the box or via a well-supported plugin.
In the remaining sections of this article we’ll dive into the details covering how we maintained feature parity during the migration and what we learned along the way.
Choosing a templating language
Since 11ty is a build-time-only tool, it’s not locked in to a syntax that compiles to browser compatible code like JSX. This freedom means there’s a mind boggling number of possible ways to write templates.
As big fans of web components and HTML, the WebC templating language got our attention — it enables the use of custom HTML elements as a templating language. Here’s how a typical WebC component file is structured:
<script> </script> <style> </style> <slot> </slot>WebC did the job and we got it to cover everything JSX did before, but not without some effort. Here are some of the things to keep in mind when migrating from JSX:
- There are a lot of different ways that 11ty can render WebC components which we needed to learn, and even after getting familiar with it, we still end up with unexpected results.
- Props can be added in 3 different ways – build only props are prefixed with @, dynamic props are prefixed with : and dynamic build only props are prefixed with :@. Unprefixed props will be treated as a standard HTML attributes.
- For functions or variables to be made available to templates they need to either be added inside a setup script or globally as a helper function. Variables from front matter can also be used but this is only available within page-level templates.
- WebC is still new, and the community is smaller than what we were used to. When things went wrong it wasn’t always obvious how to fix them.
This was actually one of the most challenging parts of the migration and we’re still settling on a consistent template format, but given the chance, we’d choose WebC all over again. It’s the closest we can get to writing in plain HTML and ultimately that’s the goal.
Bundling styles
WebC has a simple built in bundler which let’s us declare CSS files to include as part of the layout template. Here’s how that looks:
<link rel="stylesheet" href="../../node_modules/@etchteam/diamond-ui/diamond-ui.css" webc:bucket="styles"> <link rel="stylesheet" href="../styles/variables.css" webc:bucket="styles"> <link rel="stylesheet" href="../styles/base/font.css" webc:bucket="styles"> <link rel="stylesheet" :href="getBundleFileUrl('css', 'styles')" webc:keep> <link rel="stylesheet" :href="getBundleFileUrl('css')" webc:keep>This ends up outputting the two <link> tags with webc:keep:
- The core styles added to the styles bucket via webc:bucket="styles"
- Component styles which get automatically bundled via getBundleFileUrl('css')
To accompany the WebC bundler a basic Lightning CSS transform was added to the 11ty config which sends all the CSS through minification and Browserslist:
async function transformCSS(content) { if (this.type !== 'css') return content; const { code } = transform({ code: Buffer.from(content), minify: true, targets: browserslistToTargets( browserslist('> 0.25% and not dead') ), }); return code; } eleventyConfig.addPlugin(webc, { components: ['src/components/**/*.webc'], bundlePluginOptions: { transforms: [transformCSS], }, });Adding JavaScript interactivity
Considering that with Next.js every single file contained JavaScript, there was surprisingly little JavaScript that was actually required on the client-side to add interactivity.
In these cases, we found a combination of WebC and Web Components work well together.
<script> class MyComponent extends HTMLElement { } if ('customElements' in window) { customElements.define( 'my-component', MyComponent ); } </script> <div class="my-component"> <slot></slot> </div>During the static build, 11ty will pull the parts of this file apart:
- The HTML will be output wherever my-component is included on the page
- The content inside the script will be bundled with the rest of JavaScript, it will only be included once even if my-component is included multiple times.
- Once JavaScript is executed, the web component will be registered
The result: reusable static HTML content is available on the page immediately with custom Web Component logic progressively enhancing the static markup.
Implementing a service worker
Previously we used next-pwa to provide near automatic service worker capabilities out of the box.
We found no equivalent plugin for 11ty, but here’s the thing… the reason dependencies like next-pwa are helpful is because Next.js performs abstracted and wonderfully complex bundling, which makes it hard to predict which files will end up being served by the browser.
We don’t have this problem with 11ty, so we can implement a custom service worker instead.
Workbox was set up to set up a pre-cache file list for all but the blog and images:
injectManifest({ globDirectory: 'dist/', globPatterns: ['**/*.{html,js,css,woff,woff2,json}'], globIgnores: ['**/sw.js', '**/workbox-*.js', '**/blog/**'], swSrc: 'src/sw.js', swDest: 'dist/sw.js', });This replaces self.__WB_MANIFEST in the sw.js file at build time, caching for the images, blog posts and API requests are also included here.
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST); workbox.routing.registerRoute( ({ request }) => request.destination === 'image', new workbox.strategies.CacheFirst({ }), ); workbox.routing.registerRoute( ({ request }) => request.mode === 'navigate', new workbox.strategies.NetworkFirst({ }), ); workbox.routing.registerRoute( ({ url }) => url.hostname === 'f.etch.co', new workbox.strategies.StaleWhileRevalidate({ }), );All that’s left is to include the service worker in the layout file:
<script webc:keep webc:if="process.env.NODE_ENV !== 'development'"> if ('serviceWorker' in navigator) { try { navigator.serviceWorker.register('/sw.js'); } catch (error) { console.error('Service Worker registration failed:', error); } } </script>Final thoughts
Something important that hasn’t been mentioned yet is that we could have achieved similar performance gains with Next.js, but it would have required gymnastics like running bundle analysers and tweaking routing methods. In choosing 11ty we accepted that Next.js is built for bigger problems than our little static content site has, so we opted for subtraction rather than addition.
The benefits of the switch have been clear, the site noticeably performs better, and that’s not just in the browser – builds are faster, there are fewer dependencies and fewer breaking changes. It feels like the tooling has gotten out of the way, and the website is just a website again ❤️
.png)
![GPT-5.1 Instant and GPT-5.1 Thinking System Card Addendum [pdf]](https://news.najib.digital/site/assets/img/broken.gif)
