Unhandled Runtime Error: Hydration and Google Ads in Next.js
Adam C. |

If you're a web developer using Next.js and have integrated Google Ads into your site, you may encounter a puzzling runtime error that says: "Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Did not expect server HTML to contain a <iframe> in <ins>."

This seems to be the issue after upgrading to Next.JS13

Photo by Thao LEE on Unsplash

The GoogleAd Component

To set the stage, we have a GoogleAd component that encapsulates the Google Ads code. It looks like this:

import React, { useEffect } from "react";

const GoogleAd = () => {
  useEffect(() => {
    try {
      (window.adsbygoogle = window.adsbygoogle || []).push({});
    } catch (err) {
      console.log(err);
    }
  }, []);

  return (
    <div className="gAdsWrap">
        <div className="gAds">
          <ins
            className="adsbygoogle"
            style={{ display: "block" }}
            data-ad-client="ca-pub-8060693734988888"
            data-ad-slot="7114256666"
            data-ad-format="auto"
            data-full-width-responsive="true"
          ></ins>
        </div>
    </div>
  );
};

export default GoogleAd;

Here, the useEffect function is used to load (push) ads into  Slots (<ins>) when the component is mounted.

Hydration Error and Cross-browser Compatibility

The error message you encountered, "Hydration failed because the initial UI does not match what was rendered on the server," is related to the process of server-side rendering (SSR) in Next.js. It appears that in some cases, the content generated by the server differs from what's expected on the client side.

One notable observation is that the issue seems to be browser-specific. While the error occurs in Brave, it doesn't in Chrome. This inconsistency can be attributed to the fact that different browsers handle scripts and ad content differently.

When you load Google Ads using the afterInteractive strategy in Next.js, the script is injected into the HTML client-side, and it loads after some (or all) hydration occurs on the page. "After some hydration" implies that the script may load before hydration during server-side rendering (SSR). This is where the problem begins.

n Brave, which has more aggressive ad-blocking features, the client-side rendering might not render the <iframe> elements as expected, causing a mismatch between what's rendered on the server and what the client expects.

Troubleshooting

To address this issue and work towards cross-browser compatibility, it's crucial to troubleshoot and experiment with different strategies. Here are some steps you can take to diagnose and potentially mitigate the problem:

1. Switch to "beforeInteractive" Strategy

As expected, changing to 'beforeInteractive' is not a solution for this issue, because Brave still blocks the script, leading to the same hydration error and inconsistency between server-side and client-side rendering.

2. Use "lazyOnload" Strategy

This strategy loads the script later during browser idle time. Because no 'adsbygoogle' script is loaded on the server-side, this can be a viable option if you want to avoid any interference with the initial page load and allow the ads to load on a subsequent page load. It's important to note that with this strategy, the ads will not load during the initial page hydration..

3. Conditional Loading with useEffect

As a workaround, you might consider conditionally rendering the <ins> element after the component has mounted using the useEffect hook. 

const [mounted, setMounted] = useState(false);
useEffect(() => {
  setMounted(true);
}, []);
{mounted && <ins>}

However, it's important to note that this method can be problematic because window.adsbygoogle.push({}) only works when it finds the <ins> element during its initialization.

I've ultimately settled on option 2 – using the 'lazyOnload' strategy – to address my issue. While it's not a perfect solution because one slot might remain empty during the initial hydration, it aligns well with Next.js's behavior. After the first full page reload, client-side navigation becomes smooth, and adsbygoogle.js remains available throughout the session. As a result, the ad slot is consistently filled.

<Script
  id="gscript-ads"
  strategy="lazyOnload" // CANNOT Omit!!!
  src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8060693734988888"
  crossOrigin="anonymous"
/>

It's worth noting that according to Next.js documentation, 'beforeInteractive' is the default strategy. However, through my testing, I've found that you cannot omit the strategy option entirely.