
Comparing Initial Page Load Performance of Next.js and Nuxt
After working with Next.js for over two years and experimenting with various frontend frameworks like Vue, Svelte, Astro, and htmx, I’ve explored the many rendering strategies and the nuances each one uses to deliver pages to the user.
As part of my current early-developer crisis, I’ve been reassessing my tech stack and how to improve upon my own developer experience (DX) and my sites’ user experience (UX).
In the process of that, I transpiled and migrated my Next.js Turbopack monorepo into a Nuxt-based monorepo, manually refactored my components to meet Nuxt’s leaner conventions.
During that exploration, I was astronished by how fast everything felt; from dev server startup to client-side routing, everything felt almost instantaneous. Combined with features like auto-imports and a rich developer toolbar, the experience felt incredibly smooth compared to my time working with Next.js.
To further understand how Nuxt feels so fast, I explored web vital scores and the different architectures of both frameworks.
This article serves as a documentation on my journey of doing just that.
CAUTION
This article is just a synthesis and documentation of my experiences using the technologies above. I am far from being an expert in this topic, so take it with a grain of salt. YMMV.
Markup Rendering Strategies
It’s important to take note of markup rendering strategies as they are the content web crawlers use to score & index your site. Each of them have their trade-offs and usecases which should be considered based on the needs of your application.
Of course you are not locked into these rendering strategies, as frameworks like Nuxt, Next.js, SvelteKit, Astro, etc. allow you to selectively choose what strategy to use, depending on the route.
I’ve compiled a list of these rendering strategies with an intuitively-short description of the main ones and what they do and some notes on their weaknesses & strengths;
Strategy | Description | Strengths | Weaknesses | Note |
---|---|---|---|---|
Static Site Generation (SSG) | Renders web pages on the server during build time. | You can easily deploy to CDNs and aggressively cache your content (based on build hash) since they are all static. A lot of providers allow you to host static content for free without limits (GitHub Pages, Cloudflare, Netlify). ”Infinitely” scalable (delivery-wise) due to the static nature of the content. Good for SEO since all the HTML is generated. | Builds are slower: you’ll need to rebuild your site if you want to change your content (assuming you don’t fetch anything on the client). For example, a static e-commerce site has 1,000 pages, but changing a typo will require you to rebuild all of those pages. | Use this if you don’t have a server and can afford the long build times. You can also use this if your content (ie. blog posts) rarely update. Deploy to CDNs to cache your content and deliver them closest to your users. Example apps include; blogs, portfolio sites, marketing or landing pages, documentation sites. |
Server Side Rendering (SSR) | Renders web pages on the server during runtime. | Unlike SSG, you can have dynamic content like calling a database before delivering the content to the client. You don’t have to build the pages as they all happen on the server, on every request. Good for SEO since all the HTML is generated. | Rendering a page will call your server. If your data relies on a slow database call, instead of it happening once during build, it happens on every request (without caching). You need the server infrastructure to host & render your pages and it isn’t always free. | Use this if your content changes very frequently or needs to be calculated on-the-fly (ie. stock market prices). If your site’s rendering is slow, look into caching strategies or mixing different rendering paradigms. Good apps include; e-commerce sites, sites that require user authentication, news sites. |
Client Side Rendering (CSR) | Builds the JavaScript on the server first. Renders web pages on the client using JavaScript. | No need to request the server for future requests as the rendering all happens on the client. This makes your site feel like a native app (understandably so because you download the entire thing’s JavaScript). You can also easily deploy these to CDNs (React, Vue, Svelte) since you ship the JavaScript bundle to render the pages. | Typically worse for SEO since it requires JavaScript to run in order to to render the HTML and metadata tags for each page.1 Web vitals (notably First Contentful Paint) will suffer since you need to execute the JavaScript bundle before pages show up. | Use for apps that don’t need SEO since it feels the fastest for users. You will need to configure CORS if you want it to communicate with a backend on a different domain, since you don’t run requests on the server anymore (it happens in the client, typically the browser). Example apps include; internal tools, admin dashboards, game websites. |
Incremental Static Regeneration (ISR) | A hybrid of SSG and SSR where it builds static pages (either on build or runtime) before “caching” it on the server. | Allows you to have the serving speed of an SSG while removing the drawback of having to statically build the content every time. | You need to define how the “cached” pages are invalidated yourself. Furthermore, you might need to define the cache headers yourself and check if they are compatible with your CDN. Requires server infrastructure. | Use this if your framework allows it if your data doesn’t have to be realtime and can be cached. Example apps include; analytics, real-estate listings, e-commerce, dashboards that can endure a bit of staleness. |
Partial Prerendering (PPR) | Similar to ISR, but instead of the entire page, you can selectively cache static sections of the page. The dynamic portions are “streamed” in from the server. | Similar to ISR, but with section-level precision. | Requires very specific platform infrastructure. For instance, you need to use Vercel’s server & CDN platform for Next.js PPR because they have non-standard cache headers like X-Nextjs-Cache to determine caching. | Use this if you like Vendor lock-in. Example apps are similar to SSR. |
Browser Navigation Strategies
Now, when it comes to client-side navigation, it mainly affects the user experience (UX) of your users. The main two are;
- Multi Page Application (MPA)
- When you click on a link, the client sends a request to the server to request for the page and then displays it to the user
- this typically triggers a hard refresh, making you lose state and can even flashbang your users due to the flash of unstyled content (FOUT) of your CSS re-running.
- Single Page Application (SPA)
- JavaScript takes control of the navigation and renders the content on the client-side rather than asking the server
- this prevents the browser from doing a disruptive refresh and allows the application to keep state inside the browser’s memory
The Joys of Client-side Hydration
Metaframeworks like Next.js, Nuxt, SvelteKit client-side hydration a first-class default for apps built with them. But why?
Client-side hydration allows websites to hybridize their rendering to happen on the client so that navigation feels smooth and application state can stay reactive, significantly improving the UX of your site.
They do these with several strategies, but the most common one is by shipping static “dry” HTML and the JavaScript bundle then letting the browser take over once the JavaScript “hydrates” the page with interactivity.
All of this results in a fluid user experience during navigation, as it feels almost instantaneous.
Next.js Hydration
During a build, Next.js statically generates as much static pages as possible and the required JavaScript chunks. They chunk the JavaScript into very small parts so that the browser doesn’t waste time running the entire bundle and focuses on rendering the HTML first for improved SEO.
On initial request, it sends the HTML document, then the HTML is executed to fetch the CSS, and lastly the little chunks of JavaScript.
If you take a look at the <head>
tag, there are multiple instaces of <script>
to load the bundled chunks of JavaScript, in a certain order - placing the more essential ones at the top.
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/_next/static/css/230fde8ed51af13d.css" data-precedence="next"> <link rel="stylesheet" href="/_next/static/css/941584343f16feb7.css" data-precedence="next"> <link rel="stylesheet" href="/_next/static/css/5fdbd4549cd8e253.css" data-precedence="next"> <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/webpack-6ae7adb56475d72c.js"> <script src="/_next/static/chunks/e4ec3898-63cb4079e3b5e7fd.js" async=""></script><script src="/_next/static/chunks/49-af30e5096ec7b489.js" async=""></script><script src="/_next/static/chunks/main-app-f604147fffaff8c4.js" async=""></script><script src="/_next/static/chunks/app/not-found-76b7d967e22f277f.js" async=""></script> <link rel="preload" href="/_next/static/chunks/15.79f85217cd1ed200.js" as="script" fetchpriority="low"> <link rel="preload" href="/_next/static/chunks/174.c142e89ff02c7b88.js" as="script" fetchpriority="low"> <link rel="preload" href="/_next/static/chunks/389.f40338a0edc51a0a.js" as="script" fetchpriority="low"> <script>document.querySelectorAll('body link[rel="icon"], body link[rel="apple-touch-icon"]').forEach(el => document.head.appendChild(el))</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" nomodule=""></script> <link rel="preload" href="/_next/static/media/569ce4b8f30dc480-s.p.woff2" as="font" crossorigin="" type="font/woff2"> <link rel="preload" href="/_next/static/media/favicon.c7cc358c.svg" as="image"></head>
- I removed the SEO meta tags and only left the CSS, assets, and JavaScript files of the page.
Next.js Lighthouse Score
TIP
You can use pagespeed.web.dev from Google to profile your website.
Here is one of the downsides of Next.js - since the chunks are so small, your network or server becomes the bottleneck as it has to request these chunks from your server in a waterfall-like manner as shown below:
This is mitigated by putting your site behind a CDN-proxy like Cloudflare to cache the static chunks so that the client doesn’t have to request from your origin server.
Next.js Treemap
There is what the final treemap of all the code needed for the initial page load looks like on a simple page with just a basic layout (navbar & footer):
[!Information] Thanks to this aggresive chunking, the web vital for First Contentful Paint is very good. The browser can immediately start executing the small pieces of JavaScript to create the first paint.
After that, subsequent requests are made to the server before the framework caches the pages in the browser, leading to SPA-like navigation.
How Nuxt Does It
Similar to Next.js, Nuxt also bundles & chunks the JavaScript files, but not as fine-grained as Next.js as shown below:
This is how the <head>
tag looks like:
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <style>/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){[data-v-a3690aac],[data-v-a3690aac]::backdrop,[data-v-a3690aac]:after,[data-v-a3690aac]:before{--tw-duration:initial;--tw-ease:initial}}}main[data-v-a3690aac]{background-color:var(--background);gap:calc(var(--spacing,.25rem)*8);min-height:calc(100lvh - 56px);padding:calc(var(--spacing,.25rem)*4);transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4,0,.2,1)));--tw-duration:.3s;--tw-ease:var(--ease-in-out,cubic-bezier(.4,0,.2,1));display:flex;transition-duration:.3s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4,0,.2,1))}@media (min-width:40rem){main[data-v-a3690aac]{padding:calc(var(--spacing,.25rem)*8);padding-inline:calc(var(--spacing,.25rem)*8)}}@media (min-width:160rem){@media (min-width:48rem){main[data-v-a3690aac]{max-width:48rem}}@media (min-width:64rem){main[data-v-a3690aac]{max-width:64rem}}@media (min-width:80rem){main[data-v-a3690aac]{max-width:80rem}}@media (min-width:96rem){main[data-v-a3690aac]{max-width:96rem}}@media (min-width:120rem){main[data-v-a3690aac]{max-width:120rem}}main[data-v-a3690aac]{max-width:160rem}main[data-v-a3690aac]{margin-inline:auto;width:100%}@media (min-width:40rem){main[data-v-a3690aac]{max-width:40rem}}@media (min-width:48rem){main[data-v-a3690aac]{max-width:calc(100vw - 32px)}}@media (min-width:64rem){main[data-v-a3690aac]{max-width:calc(100vw - 32px)}}@media (min-width:80rem){main[data-v-a3690aac]{max-width:calc(100vw - 414px)}}@media (min-width:96rem){main[data-v-a3690aac]{max-width:min(100vw - 710px,1536px)}}}aside[data-v-a3690aac]{display:none}@media (min-width:80rem){aside[data-v-a3690aac]{display:grid;min-width:160px;width:160px}}@media (min-width:96rem){aside[data-v-a3690aac]{min-width:300px;width:300px}}#main[data-v-a3690aac]{width:100%}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}</style> <style>/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){[data-v-f4cd5757],[data-v-f4cd5757]::backdrop,[data-v-f4cd5757]:after,[data-v-f4cd5757]:before{--tw-border-style:solid;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}header[data-v-f4cd5757]{align-items:center;background-color:var(--background);border-bottom:1px var(--tw-border-style) #0000;border-left:1px var(--tw-border-style) #0000;border-right-color:#0000;border-top-color:#0000;display:flex;flex-shrink:0;gap:calc(var(--spacing,.25rem)*2);pointer-events:auto;position:sticky;top:calc(var(--spacing,.25rem)*0);width:100%;z-index:50}@supports (color:color-mix(in lab,red,red)){header[data-v-f4cd5757]{background-color:color-mix(in oklab,color-mix(in oklch,var(--background)90%,var(--color-white)12%)60%,transparent)}}header[data-v-f4cd5757]{--tw-backdrop-blur:blur(var(--blur-xl,24px));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4,0,.2,1)));--tw-duration:.3s;transition-duration:.3s}header[data-v-f4cd5757],header[data-mobile-open=true][data-v-f4cd5757]{-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}header[data-mobile-open=true][data-v-f4cd5757]{background-color:var(--sidebar);border-bottom-color:var(--border);--tw-backdrop-blur: ;--tw-duration:.5s;transition-duration:.5s}header[data-v-f4cd5757]{height:var(--height)}@media (min-width:48rem){.wrapper[data-v-f4cd5757]{max-width:48rem}}@media (min-width:64rem){.wrapper[data-v-f4cd5757]{max-width:64rem}}@media (min-width:80rem){.wrapper[data-v-f4cd5757]{max-width:80rem}}@media (min-width:96rem){.wrapper[data-v-f4cd5757]{max-width:96rem}}@media (min-width:120rem){.wrapper[data-v-f4cd5757]{max-width:120rem}}@media (min-width:160rem){.wrapper[data-v-f4cd5757]{max-width:160rem}}.wrapper[data-v-f4cd5757]{display:flex;margin-inline:auto}@media (min-width:40rem){.wrapper[data-v-f4cd5757]{max-width:40rem}}@media (min-width:48rem){.wrapper[data-v-f4cd5757]{max-width:calc(100vw - 32px)}}@media (min-width:64rem){.wrapper[data-v-f4cd5757]{max-width:calc(100vw - 32px)}}@media (min-width:80rem){.wrapper[data-v-f4cd5757]{max-width:calc(100vw - 414px)}}@media (min-width:96rem){.wrapper[data-v-f4cd5757]{max-width:min(100vw - 710px,1536px)}}.wrapper[data-v-f4cd5757]{align-items:center;gap:calc(var(--spacing,.25rem)*6);padding-inline:calc(var(--spacing,.25rem)*4);width:100%}.logo[data-v-f4cd5757]{--tw-translate-x:-50%;left:50%;position:absolute;translate:var(--tw-translate-x)var(--tw-translate-y)}@media (hover:hover){.logo[data-v-f4cd5757]:hover{animation:var(--animate-wink,wink-f4cd5757 .5s ease-in-out)}}@media (min-width:40rem){.logo[data-v-f4cd5757]{left:calc(var(--spacing,.25rem)*0);--tw-translate-x:calc(var(--spacing,.25rem)*0);position:static;translate:var(--tw-translate-x)var(--tw-translate-y)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes wink-f4cd5757{0%{transform:rotate(0)}20%{transform:rotate(15deg)}to{transform:rotate(0)}}</style> <style>.line[data-v-23f5b105]{height:1px;transition:all .3s cubic-bezier(.22,1,.36,1);width:24px}.top-closed[data-v-23f5b105]{transform:translateY(-3px) rotate(0)}.top-open[data-v-23f5b105]{transform:translateY(1px) rotate(45deg)}.bottom-closed[data-v-23f5b105]{transform:translateY(3px) rotate(0) scaleX(1)}.bottom-open[data-v-23f5b105]{transform:translateY(-1px) rotate(-45deg)}</style> <link rel="stylesheet" href="/_nuxt/entry.B-Umql9l.css" crossorigin=""> <style>@layer base {:where(.i-tabler\:home-filled){display:inline-block;width:1em;height:1em;background-color:currentColor;-webkit-mask-image:var(--svg);mask-image:var(--svg);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m12.707 2.293l9 9c.63.63.184 1.707-.707 1.707h-1v6a3 3 0 0 1-3 3h-1v-7a3 3 0 0 0-2.824-2.995L13 12h-2a3 3 0 0 0-3 3v7H7a3 3 0 0 1-3-3v-6H3c-.89 0-1.337-1.077-.707-1.707l9-9a1 1 0 0 1 1.414 0M13 14a1 1 0 0 1 1 1v7h-4v-7a1 1 0 0 1 .883-.993L11 14z'/%3E%3C/svg%3E")}:where(.i-tabler\:lock-filled){display:inline-block;width:1em;height:1em;background-color:currentColor;-webkit-mask-image:var(--svg);mask-image:var(--svg);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3'/%3E%3C/svg%3E")}:where(.i-tabler\:settings-filled){display:inline-block;width:1em;height:1em;background-color:currentColor;-webkit-mask-image:var(--svg);mask-image:var(--svg);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M14.647 4.081a.724.724 0 0 0 1.08.448c2.439-1.485 5.23 1.305 3.745 3.744a.724.724 0 0 0 .447 1.08c2.775.673 2.775 4.62 0 5.294a.724.724 0 0 0-.448 1.08c1.485 2.439-1.305 5.23-3.744 3.745a.724.724 0 0 0-1.08.447c-.673 2.775-4.62 2.775-5.294 0a.724.724 0 0 0-1.08-.448c-2.439 1.485-5.23-1.305-3.745-3.744a.724.724 0 0 0-.447-1.08c-2.775-.673-2.775-4.62 0-5.294a.724.724 0 0 0 .448-1.08c-1.485-2.439 1.305-5.23 3.744-3.745a.722.722 0 0 0 1.08-.447c.673-2.775 4.62-2.775 5.294 0M12 9a3 3 0 1 0 0 6a3 3 0 0 0 0-6'/%3E%3C/svg%3E")}}</style> <link rel="preload" as="image" href="/_ipx/s_32x32/favicon.svg"> <link rel="preload" as="image" href="/_ipx/s_32x32/favicon.svg"> <link rel="modulepreload" as="script" crossorigin="" href="/_nuxt/BIwHCmKY.js"> <link rel="modulepreload" as="script" crossorigin="" href="/_nuxt/DyogVLXt.js"> <link rel="modulepreload" as="script" crossorigin="" href="/_nuxt/DlAUqK2U.js"> <link rel="preload" as="fetch" fetchpriority="low" crossorigin="anonymous" href="/_nuxt/builds/meta/8e9c5379-81e6-47d6-83ea-955417ec967b.json"> <link rel="prefetch" as="script" crossorigin="" href="/_nuxt/TEox3_1n.js"> <link rel="prefetch" as="style" crossorigin="" href="/_nuxt/default.GNPMa9Hc.css"> <link rel="prefetch" as="script" crossorigin="" href="/_nuxt/P8x7qHJU.js"> <link rel="prefetch" as="script" crossorigin="" href="/_nuxt/Sf__LlnF.js"> <link rel="prefetch" as="script" crossorigin="" href="/_nuxt/BwoWIfDV.js"> <link rel="prefetch" as="script" crossorigin="" href="/_nuxt/CYzyH2rw.js"> <link rel="prefetch" as="script" crossorigin="" href="/_nuxt/CQO5ttHh.js"> <link rel="stylesheet" crossorigin="" href="https://aru.ojou.de/_nuxt/default.GNPMa9Hc.css"> <link rel="modulepreload" as="script" crossorigin="" href="https://aru.ojou.de/_nuxt/6PKQEePe.js"> <link rel="modulepreload" as="script" crossorigin="" href="https://aru.ojou.de/_nuxt/U-2IZ-e9.js"> <link rel="modulepreload" as="script" crossorigin="" href="https://aru.ojou.de/_nuxt/BzLVI_JE.js"> <link rel="modulepreload" as="script" crossorigin="" href="https://aru.ojou.de/_nuxt/BW0L0ZnU.js"></head>
As you can see, it has WAY more prefetching, so the code isn’t required for the initial render. Nuxt even inlines a lot of the CSS needed for the site so that it doesn’t have to fetch a separate CSS bundle.
Nuxt Treemap
My Nuxt site has heavier dependencies compared to the Next.js one. It has Pinia for global state, Nuxt Content for .mdc
rendering, and a good number of Reka UI components. But despite all that, the bundle size is smaller than Next.js!
- the only time these library chunks are fetched (notably Nuxt Content’s SQLite WASM code) are when the page actually uses it, afterwards, it’s cached in the browser
Nuxt Lighthouse Score
One thing to note, however, is that the First Contentful Paint here is slower than Next.js, mainly because of the initial requests to fetch & then execute the JavaScript takes longer.
- the FCP here is
0.6s
slower than Next.js from numerous test runs, but otherwise the performance is similar.
Similar to Next.js, after the initial request of the page, it gets cached in the browser by Nuxt and subsequent requests have a SPA-like experience.
Final Thoughts
Both Next.js and Nuxt are great options for having fluid reactivity in a web application while achieving a good SEO score. They have similar performance metric scores, so for most people, it’s a matter of picking whether to use Vue or React (syntax and ecosystem-wise).
One thing you need to note about Nuxt is that the initial bundle sizes are bigger, especially for pages with larger dependencies (like Nuxt Content), however, once they are downloaded for the first time, they’re cached and navigation feels instant.
I’ll personally continue to use Nuxt since I enjoy the control I have over the rendering & client-side hydration (on a per-route and per-component basis). And also, the high-quality module ecosystem, dev tooling, and open source community is light years away from React.
However, that isn’t to say Next.js (and React) doesn’t have its merits. It’s highly unopinionated and very modular. For the folks that enjoy creating their own conventions and setting up everything themselves manually, they’ll fit right at home.
But nowadays, I’ll continue to pnpx nuxi module add
and enjoy high quality open source tooling that isn’t tied to a multi-million dollar PaaS.
Footnotes
-
This is not the case in recent times because Google can index SPA pages albeit slower due to the crawler’s “crawl budget”. ↩