Next.js is a powerful React framework that provides several rendering methods to optimise web applications for performance, scalability, and user experience. Next.js also comes with advanced features like Server Side Rendering (SSR) and Static Site Generation (SSG) that help to improve applications' efficiency and user experience. In this write-up, we’ll explore these concepts and a challenge I recently ran into with environment variables in Next.js website builds.
Server-Side Rendering is an application’s ability to generate the HTML pages on a server for each page request. The web browser sends a request to the server, which responds by delivering a wholly displayable page. Pages are generated at build time and then sent to the client as static pages. SSR is suitable for web pages that display real-time or frequently changing information.
Some advantages of SSR include better SEO (since search engines don’t have to evaluate a bundle of JavaScript to render the page) and flexibility, as SSR can handle connections to various backend services and complex logic. Some drawbacks include slower load times because pages are generated on each request.
Static site generation involves creating a complete static HTML website from raw data and a collection of templates. It generates individual HTML pages and prepares them for delivery to users in advance. SSG is like pre-rendering your website pages so visitors can access them immediately, resulting in a faster, more efficient browsing experience. It's ideal for blogs or documentation sites with relatively static content.
Advantages of SSG include faster performance as the pages are pre-built and SEO friendly, because the pages are easy to index. It’s also cost-effective, as hosting static files is usually cheaper than maintaining dynamic server-side resources. The major drawback of SSG is the absence of server-side logic, as content is fixed at build time and would need a rebuild to update.
My team recently worked on a web application where everything worked perfectly in development. Pages loaded smoothly, data loaded as expected, and although we used a bit of old-school technology, everything felt solid and predictable. But as we prepared for deployment, the wheels started to wobble. This prompted a dive into the quirks of Next.js builds, especially when environment variables are involved.
During development, environment variables feel tame. process.env.MY_API_URL behaves just like any other config. I had variables for my API endpoints, feature flags, and third-party tokens, all stored neatly in my .env file.
SSR runs on the server at request time, so I assumed environment variables would just work, and they did as long as I used them correctly. This led us to create a config.ts file that exported a few .env constants at the top level. It worked locally, but in production builds, those imports were sometimes hoisted (loaded) too early, especially when used in SSG components. The result? SSR pages referencing undefined because the variable was never injected into the bundle as I expected. The fix was to avoid top-level constant exports and instead reference process.env directly inside functions or at runtime. It felt messy, but it worked. This also meant making calls to external endpoints in the pages/api folder, not in SSG or client-side components.
Public vs Private Variables Next.js distinguishes between public (NEXT_PUBLIC_) and server-only environment variables, but it doesn’t enforce how you use them.
I had mistakenly used process.env.MY_API_URL inside client-side code. Locally, it worked fine. But during the production build, that variable was stripped out because it wasn’t prefixed with NEXT_PUBLIC_. Result: blank API URL and failed network requests on the frontend. No warnings. No errors. Just silent failure. The solution? Prefix variables you need in the browser with NEXT_PUBLIC_, and double-check that anything you use on the server doesn’t leak sensitive keys.
**Final Thoughts: **Next.js is powerful. It gives you Server Side Rendering, Static Site Generation, and client hydration in one neat package. But with that power comes complexity. And nowhere is that complexity more subtle than with environment variables. What feels like a simple process.env.X can hide a landmine when rendered at the wrong time, in the wrong place.
So if you’re building with SSR or SSG in Next.js: tread carefully. Know your runtime!