Optimizing Next.js cold starts for Vercel

In this post i will show some tricks to decrease your Next.js functions cold starts
profile photo
Tommy
This post is a summary of the tricks I used to optimize the akarso.co cold start times, Akarso is open source so you can see how I did in detail if you want.

End result of this post

I managed to optimize Akarso Next.js app from 5+ seconds cold starts to about 1 second but there is a catch: these are the execution times (profiling starts only with the first line of code evaluated), there will be an additional period of time spent provisioning the function. In this example app provisioning time is about 1 second.
I measured the latency of the lambda every 10 minutes in the same region of the deployed code and as you can see the latency was about 2.3 seconds (while the execution time was about 1.2):
Image without caption

Import only what you need

It’s very easy to bloat your function size with code that is not useful. You should clearly separate code used by your pages. Vercel will only require files that are needed for your page. If you group your code in big utils.js files your pages will end up very bloated.
Here is an example profile with this issue, despite being an API handler this function is importing the _app file, probably because that file is exporting some utility used by the API handler:
Image without caption

How to profile your functions cold starts

I developed a tool to make profiling Next.js cold starts super easy on Vercel.
To use it you just need to add a vercel.json file in your Next.js folder (next to package.json) with the following contents:
json
{ "builds": [ { "src": "package.json", "use": "https://profile-next-cold-starts.vercel.app/builder.tgz" } ] }
After you redeploy the app you will be able to download a profile of your function appending a search param, for example:
plain text
https://my-app.vercel.app/api/auth?vercel-profile-cpu // downloads a CPU profile https://my-app.vercel.app/api/auth?vercel-profile-require // downloads a profile of `require()`
💡
The profiles only show the time spent since the lambda function starts executing javascript, there may be additional time spent loading your function by AWS.

Node.js import is very slow

In the CPU flame chart below you can see that a big part of the cold start is spent resolving and loading es modules.
ES modules resolution is asynchronous, this means that importing many modules at the same times will fill the event loop very quickly, wasting CPU cycles taking off and putting tasks in the event loop.
Image without caption
This analysis has been confirmed by Vercel tech lead Javi Velasco, I am excited Vercel is focused on decreasing cold starts internally too!
💡
Next.js cannot require these packages as usual because their package.json contains "type": "module", forcing Next.js to import or bundle them.

Use esmExternals: false

We can solve this problem by bundling the ESM dependencies and setting the experimental.esmExternals option to false.
javascript
const nextConfig = { experimental: { esmExternals: false, }, };
As you can see below, this change decreased the cold start from 4.5 seconds to 1.6. Now the majority of the time is spent on require so that will be our next optimization.
Image without caption

require is slow too

Passing ?vercel-profile-require with this tool I profiled what packages are the slowest to be loaded.
Image without caption
As you can see the whole cold start is caused by require. Next itself is taking about 500ms but we can’t do anything about it for now so let’s focus on the rest.
Just @sentry/nextjs is taking 400ms to be required, that seems a lot for an error reporting library so I investigated if there was a lighter alternative to report errors.

Use the Edge version of @sentry/nextjs

I explored other error reporting libraries but all of them had the same issues.
I dived into the sentry/nextjs source code and found out that it already has a lighter way to report errors made just for the edge environment (Vercel Edge and Cloudflare Workers).
From the code it looks like the edge runtime is compatible with Node.js too, it simply uses fetch to send errors to Sentry.
So we can use the following next.config.js to replace the Node.js sentry adapter with the edge one.
javascript
const nextConfig = { webpack: (config, { dev, isServer }) => { if (isServer) { config.resolve.alias["@sentry/nextjs"] = require.resolve( "@sentry/nextjs/cjs/edge" ); } return config; }, };
💡
This trick is still experimental and could cause issues in production, use it at your own risk.
The result of that line change is 400ms shaved off our cold start:
Image without caption

Import from subpaths of your dependencies

Now the function cold start is only one second. The next step would be to optimize your other dependencies.
Sometimes you can import only a subset of your dependency, for example in date-fns you can only import the function you need from date-fns/format, making requiring it much faster.
Unfortunately the slow package in my profile is @supabase/supabase-js which does not offer this paradigm, you must require the realtime and database modules even if you only need the authentication part.
There is some discussion about releasing a tree shakeable version of the supabase client in the 2.0 release, leave your opinion here.

If SEO isn’t important, disable SSR

SSR consists in rendering your Next.js pages to HTML and hydrating them on the client. This process helps you have better SEO and First Contentful Paint time.
But if you are creating a dashboard that cannot be crawled by Google and your cold start time is 4 seconds, SSR isn’t really helping you.
SSR causes your lambda functions to import all React libraries and components used in the page, vastly increasing the function cold start.
Usually React component libraries are very big (with icons libraries being literal black holes), by removing these dependencies from your Next.js functions you can really decrease cold starts Considerably.

Disable SSR with this one little trick (big speedup ⚡️)

I developed a Next.js plugin called elacca that can remove all your client-related code from your lambda functions.
💡
This plugin only works with the pages directory for now
To use it simply add the following in your next.config.js:
javascript
const { withElacca } = require('elacca') /** @type {import('next').NextConfig} */ const config = {} const elacca = withElacca({}) module.exports = elacca(config)
To disable SSR in your pages you can add the skip ssr directive at the top of your page files, it works best if you also add it to your _app page and all pages that make use of getServerSideProps:
javascript
// pages/index.js 'skip ssr' export default function Home() { return <div>hello world</div> }
💡
If you use Next.js for your landing page or SEO-related pages, don’t add ‘skip ssr’ to _app, instead use dynamic to dynamically import any heavy components.
This plugin will remove all your React-related code via dead code elimination:
  • When a page has a "skip ssr" directive, this plugin will transform the page code so that
  • On the server, the page renders a component that returns null, Next.js will then automatically remove unused imports
  • On the client the page component is replaced with one that renders null until the component mounts, removing the need to hydrate the page
This plugin should vastly decrease the size of your functions.
Here is a before and after skip ssr of a simple function, as you can see we shaved off all the @nextui-org React components off our profiles, making the function almost 1 second faster.
Image without caption
Image without caption

Sponsors

This blog post is kindly sponsored by myself: Holocron.
If you use markdown at your company and want to make it easier for non-technical team members to write docs, try Holocron!
You can get a Notion-like experience (with real-time collaboration!) while syncing all your docs with GitHub
Image without caption
PS: This blog post was written in Notion and published with Notaku, another one of my projects.
Check that out too if you like Notion.
Related posts
post image
How to make Next.js plugins work with Turbopack, the new Next.js --turbo option. No need to rewrite them in Rust!
post image
This guide shows how you can use Holocron to let contributors suggest edits to your Markdown based website (like Docusaurus or Nextra) using an easy to use WYSIWYG editor.
post image
A list of my favourite Framer templates for landing page websites, free and paid. For digital businesses and startups.
Powered by Notaku