A few days ago, I went to my freshly redesigned website having not viewed or edited it since early the previous day. I received a DNS issue as shown in Figure 1. This error was gone if I refreshed the page. However, most people who get an error going to a website will assume that the website is down so the error needed to be addressed.
Diagnosing the Problem
I knew that refreshing the page resolved the problem and that the issue would not arise if I re-attempted to view the website soon after visiting it. This indicated that the problem likely did not have to do with standard speed benchmarks that we concern ourselves with in web development, e.g. first content paint. Nevertheless, I wanted to rule out first content paint being an issue so I did some speed testing on the deployed website. As expected, the times were great with the first content painted within .54 seconds and the entire website loading in 1.5 seconds.
I was unable to find any documentation of others having this problem despite my having a fairly standard NexJS website without a sprawling dependency list or complicated logic. I had deployed many websites before with Vercel with no issues so I knew it was something with my project that was different than my previous ones. I was using two packages that I had never used before framer-motion
and react-rough-notations
. I considered the possibility that these dependencies may be the culprits because they drastically slowed down compilation when I was developing locally, taking roughly 5 seconds to load content after a fresh restart with next dev
.
Based on my observations, I decided to focus on optimizing the bundling of my website packages in any way I could. In the back of my mind, I thought that these issues should not cause the DNS error I saw before but I had no other theories and had to start on a solution.
A Digression on Serverless Computing
Vercel1 deployments are, to a first approximation, a layer on top of AWS Lambdas, a serverless computing provider. While I have not worked on any large-scale projects that provisioned large cloud systems, I studied them during my Masters and they are perhaps the most underrated technical achievement fueling the Internet today.
In general, cloud computing provides for economies of scale reducing the cost of server management and the aggregation of the best technical expertise. Serverless functions are another layer of innovation on top of cloud computing. They are extremely optimized down to the kernel level so that they can cold-start with 100s of milliseconds or even microseconds of a request. Thus, serverless functions can start at request time rather than running permanently (Figure 2). The reason I don’t have to pay for deploying my “hobby” projects (with low traffic) is that it costs virtually nothing2 to run these serverless functions.
Solutions
Based on the documentation and the results of the bundle analyzer, I did the following:
- I added a Suspense wrapper to my website’s content with a loading element.
- I knew this would not solve my issue since my first-paint speed was good, but using Suspense to have a loading UI is best practice.
- Added the
optimizePackageImports
flag and applied it toframer-motion
andreact-rough-notations
. From the docs, “This option will only load the modules you actually use, while still giving you the convenience of writing import statements with many named exports.”- I was especially interested in its performance with
framer-motion
since the dependency JavaScript is large and there is not much that you can do to reduce it3 since you cannot import specific animations.
- I was especially interested in its performance with
- I converted
framer-motion
calls to use vanilla CSS for simple animations.- I used
framer-motion
when vanilla CSS could accomplish the same effects since I already had the dependency. I realized that I could removeframer-motion
from the necessary page load by using vanilla CSS when paired with the next optimization.
- I used
- I applied component lazy loading and skipped SSR4 for my animated components based on the docs.
🛬 Results
After applying the optimizations my server-side bundle size (parsed not static) was reduced by 19.1% as shown in Figure 4. Since these updates, my website has worked properly even after prolonged periods without requests.
🚀 Why not Astro
When first building my new website, I attempted to use Astro instead of NextJS. Since my website is all static, Astro would perhaps be a better technical fit especially since their implementation of Server Islands which is similar to NextJS 14’s Partial Prerendering would allow me to adds dynamic components when necessary. There are a lot of things I love about Astro. However, I struggled to use stateful components in Astro and ultimately had to use Next.
🪞 Reflections
The automatic optimizations that enable the rapid deployment of efficient websites like my own are truly stunning. A combination of serverless optimization, package bundle optimizations, and component optimizations in React metaframeworks allow me to focus on the content first. When I made my first website some years ago, I manually minified all my images and converted them to better formats for web viewing. With modern frameworks like Next, there are built-in Image
components that perform optimizations like this for you.
We stand on the shoulders of giants.
Footnotes
My website is built with NextJS so Vercel was the natural choice for deployment.↩︎
Pricing for AWS Lambdas are billed at the 1-millisecond granularity. As of writing:
- $0.20 per million requests
- $0.0000166667 per GB-second of compute
This implies running 6000 1 GB Lambda function for one second costs $0.10.↩︎
This is not entirely true. There are some tools Framer provides to reduce the bundle size but they have limitations. As Framer points out, this normally shouldn’t be necessary because most bundlers apply “tree shaking” to reduce the bundle to just what is used.↩︎
Server Side Rendering (SSR) refers to when the HTML that the client is served is generated on the server, often using a database or other external APIs.↩︎