Next.js with and without JavaScript

frontend, nextjs, react, ssg, ssr, styled-components

Wait, did you say without JavaScript?
We are React developers, why should we care about making sites that work without JavaScript?


  • Everyone sees your bare HTML at some point! (Not just users with JavaScript disabled)
  • Performance (especially on lower-end devices with a slow network)
  • Cumulative page layout shift
  • SEO

How does Next.js really work?

Static Site Generation (SSG)

If you are using Next.js (pages router), then its likely that the excellent SSG and SSR features were one of the reasons that made you consider it for your project. Specifically I would like to talk about SSG in this post.

Basically if you don't know, when you run next build, Next.js will try to pre-render as much of each page as possible. This is what gets sent to your users when they first access the site. Since there is already some content thats been pre-rendered, React only needs to rehydrate whats already there. This means there is less work done by your browser which translates to quicker execution times and better user experience.

But there is a much bigger reason that SSG pages appear to load quicker. The secret lies in the sheer magnitude of the JavaScript payload that most sites require. This bundle will include:

  • your code
  • React
  • Next.js
  • all of the other crap in your package.json

The size of this bundle will dwarf the pre-rendered HTML. As an example of bundle sizes, for any given page on this blog you are reading, the JavaScript bundle will be roughly 10 times the size of the static HTML.

In a traditional client rendered SPA, the index.html file contains almost zero markup. Usually it will just have an empty <div id="root"></div> that React will append DOM nodes on to. This means most (if not all) of the JavaScript needs to be downloaded before anything can be rendered at all. By pre-rendering the HTML, the browser can display something without having to wait for any JavaScript, this can improve the first contentful paint (FCP). Using this tactic Next.js lets us get away with having large bundle sizes whilst delivering the user a decent experience.

Responsive SSG

Although for the most part Next.js takes care of SSG for us, it does require some careful thought when we implement responsive design. One thing to consider with SSG is that everyone gets sent the same static HTML. Whether you're on desktop or mobile you will be given the same initial DOM. This can be a problem if you rely on JavaScript to make your website responsive. Lets say your site uses a hook like useWindowSize(), and depending on the width of the window you will render either the desktop layout or the mobile layout. Well this can only happen after the JavaScript bundle has been fetched and React has rehydrated the page. By which time the user will have already seen the bare static HTML. And so will potentially experience a brief flicker as the correct layout is rendered (probably with a few rehydration errors). One solution could be to simply not render anything until rehydration has finished. But of course that defeats the object of SSG in the first place.

My advice is to rely solely on CSS for your responsivity (via media queries). This way both the desktop layout and mobile layout will be present in the initial HTML that gets sent out to every device.

But wouldn't this make my HTML file bigger?

Yes. But not by that much. You have to remember that the JavaScript bundle will usually be at least an order of magnitude bigger.

Progressive Design Enhancement

Saying all that, if you are a fan of rather creative designs that require dynamically calculating the positions of elements (like using getBoundingClientRect() for example), you must also further consider how your designs will work without the presence of JavaScript. For example on this blog I often use an annotation component that draws a line connecting a sliver of code with a message. For this I need to use JavaScript to get the position of elements as they depend on the screen size of the end device and therefore are impossible to know beforehand. To solve this, I have another design which does not require any JavaScript to work properly. I simply assign a number in the top left corner of each message bubble that corresponds to the number given to the code segment it refers to. This is the design that the static HTML ships with before ultimately it gets rerendered on the client (assuming JS is enabled!). If I simply excluded the annotations entirely from the initial SSG'd HTML, when the JavaScript eventually did load they would be freshly inserted into the page causing layout shift. This is bad for UX, SEO and all manner of things. Not to mention, some people (who can blame them) browse with JavaScript disabled so they won't be able to see anything at all! For these reasons its worth thinking of a design that can handle both scenarios reasonably well, lets call it progressive design enhancement.

A code annotation component before hydrationA code annotation component before hydration

Hydration & First client render

A code annotation component after hydrationA code annotation component after hydration

As a general rule, when JavaScript is not available always try to show something, and preferably something that takes up the same amount of vertical space so as not to cause a layout shift.

How to detect if JavaScript is loaded/enabled?

When Next.js performs SSG it does not execute any effect hooks in your code. If a useEffect hook executed at least once then this means JavaScript must be loaded. We can add a CSS class to the root node to indicate this.

This will only be executed on the client once the JavaScript has loaded.
// _app.jsx
function MyApp({ Component, pageProps, router }) {
const [mounted, setMounted] = useState(false)
useEffect(1() => setMounted(true), [])
return (
<Component className={mounted ? "has-js" : "no-js"} {...pageProps} />
This will only be executed on the client once the JavaScript has loaded.

From anywhere in our App we can show and hide components easily depending on if there is JavaScript.

export const ShowWhenJSLoaded = styled.span`
display: none;
.has-js & {
display: contents;
export const ShowBeforeJSLoaded = styled.span`
display: none;
.no-js & {
display: contents;
const MyComp = () => (
<Placeholder />
<SomeComponentThatNeedsJS />

How to test your site works without JavaScript?

Disabling JavaScript with Chrome dev tools.Disabling JavaScript with Chrome dev tools.

To manually disable JavaScript in the developer tools type Ctrl+Shift+p to open the command pallet and then search for disable JavaScript. If you now reload the page you will be seeing what non JavaScript users and search engine crawlers see.

If you use Styled Components, its worth checking that all your links function at this point. When using the next/link component around a styled component you will find that the passHref must be passed for the links to work without JavaScript. This is important for SEO because search engine crawlers use links on your site to discover its pages.

If your links aren't working without JavaScript, try passing this prop.
// Example
export const Comp = () => {
return (
<Link 1passHref href={`/index`}>
<MyStyledComponent>Click Me!</MyStyledComponent>
const MyStyledComponent = styled.a`
padding: 20px;
If your links aren't working without JavaScript, try passing this prop.

Help! SSG Isn't working!

Be aware that when using yarn dev you are not seeing the finished product. This is because SSR/SSG is not done in development mode. To be able to test your app locally you need to run yarn build && yarn start which will generate the production code. Unlike other workflows, with Next.js and any other SSR/SSG supporting framework, production code does not just mean "optimised" or "minified", the way your code is delivered and performs will be fundamentally different. In particular, hydration related bugs will only be apparent in the production build.