Next.js: Scripts with async in Head are executed twice

Created on 14 Oct 2019  ·  18Comments  ·  Source: vercel/next.js

Bug report

Describe the bug

Scripts in the Head are run twice if marked async.

To Reproduce

Steps to reproduce the behavior:

  1. Create next app
  2. Create file at /static/log.js with console.log('hello'); inside
  3. Attach to a page (make sure to add the async attribute)
<NextHead>
  <script src="/static/log.js" async></script>
</NextHead>
  1. Run site
  2. See duplicate console.log in console.

Expected behavior

Scripts should only run once.

Screenshots

Screen Shot 2019-10-14 at 3 34 44 PM

System information

  • OS: macOS
  • Version of Next.js: latest 9.1.1
good first issue bug needs investigation

Most helpful comment

Thanks for the feedback! I ended up wrapping the script in the following conditional so it only renders client-side:

{process.browser && <script id="hire-embed-loader" defer async src="https://hire.withgoogle.com/s/embed/hire-jobs.js" /> }

All 18 comments

I'm very new to nextjs, but this issue peaked my interest.

I've managed to take a quick look at the issue and found that it also happens for script tags with the defer attribute as well.

Testing it out locally, it looks like it happens when the client mounts and the script tag goes from <script src="/logs.js" async=""></script> to <script src="/logs.js" async="true"></script>. Not sure whether React is treating the async attribute as a boolean prop, but not sure how it ends up resolving it to true either.

I tracked it down to this line in the next-server/lib/head.tsx file. Removing the updateHead function from the handleStateChange prop, stopped the double call. I appreciate this wouldn't be a fix, as the updateHead is there for a reason.

Hopefully this little investigation can help track down the true source of the issue.

On the client, a NextHead update following initial hydration will change the value of the async prop from "" to true, which will cause the script to be re-executed.

A brute-force fix for this would be to read in the props when invoking cloneElement() on the ReactElements passed as children to NextHead, and coerce async / defer to "" for true, undefined for false:

    .map((c: React.ReactElement<any>, i: number) => {
      const key = c.key || i
      const props = { key }
      if ('async' in c.props) props.async = c.props.async ? '' : undefined
      if ('defer' in c.props) props.defer = c.props.defer ? '' : undefined
      return React.cloneElement(c, props)
    })

This is not a real solution either, but it might help narrow down the issue.

A temporary work around is to probably render this instead:

<script src="/static/log.js" async="async"></script>

A temporary work around is to probably render this instead:

I gave that a try when looking into the issue and that still results in the script been re-executed (I should have mentioned that before).

Again, the script element goes from <script src="/logs.js" async=""></script> to <script src="/logs.js" async="async">.

If you end up going with @developit suggestion, it might be worth taking a look at other boolean attributes for script tags.

I'm experiencing the same issue on a client's website. They are using Google Hire for job openings so we are loading the following script in

<script id="hire-embed-loader" defer async src="https://hire.withgoogle.com/s/embed/hire-jobs.js" />

On a client-side render, it loads as expected. On a server-side render, it runs twice resulting in two identical iframes. I tried adjusting async to async="async" which did not do the trick.

Has anyone found a solution that does not involve the temporary workarounds?

@dsmikeyorke a temporary workaround is to have a global var init at the top of the file...

/* see if this file already ran, if so, do nothing */
if (window.hasOwnProperty('myScriptNameInit') && window.myScriptNameInit){
  return;
}

/* else if it hasn't run, make that it has. */
window.myScriptNameInit = true;

While not ideal, this is a workaround to stop the file from running twice.

Maybe putting the scripts into _document.js resolves the issue temporarily since its contents should only be rendered during SSR and not being hydrated? I might be wrong, just guessing.

Thanks for the feedback! I ended up wrapping the script in the following conditional so it only renders client-side:

{process.browser && <script id="hire-embed-loader" defer async src="https://hire.withgoogle.com/s/embed/hire-jobs.js" /> }

@dsmikeyorke oh that's a nifty way to handle that. Any side effects to doing this on a ton of script inclusions?

@jonkwheeler I only have this conditional on one script. I have not encountered any side effects and it cleared up the double load issue!

@dsmikeyorke I think I'm gonna go with @timneutkens suggestion here - https://github.com/zeit/next.js/issues/2177#issuecomment-536178575

Very similar to what you said, and to what he said. He says _not_ to assign it to a variable so it tree shakes away, so I'd love confirmation if this is bad or not. I can't imagine one var declaration harming anything, but wanted to double check.

const isBrowser = typeof window !== 'undefined'

.......

{isBrowser && <script src="1" />}

{isBrowser && <script src="2" />}

{isBrowser && <script src="3" />}

Edit:

Only thing that has worked for me is the following, and creating a var like const isBrowser = typeof window !== 'undefined'

{typeof window !== 'undefined' && <script src="1" />}

or

const isBrowser = typeof window !== 'undefined'

return (
  <NextHead>
    {isBrowser && <script src="2" />}
  </NextHead>
)

React forbids difference between SSR and CSR on hydration:

React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them. In development mode, React warns about mismatches during hydration. There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.

Source: https://reactjs.org/docs/react-dom.html#hydrate

So the solutions outlined here might lead to weird issues. I already had such issues with DOM elements not matching up anymore and having wrong attributes.

Loading the script in the following Head component works for me so far. Fingers crossed.

const NonSSRHead = dynamic(
    () => import('next/head'),
    { ssr: false }
);

Loading the script in the following Head component works for me so far. Fingers crossed.

const NonSSRHead = dynamic(
    () => import('next/head'),
    { ssr: false }
);

I'm using this workaround now. Though I still get a console warning Warning: Expected server HTML to contain a matching <header> in <div>. from time to time locally after hot reload. It then seems to break some styles. A page reload fixes it.

Hello @all,

I'm getting the same issue as mentioned above.
Does anyone know if a workaround exists?
Thank you.

I personally haven’t found anything to get it working. I ended up just loading what I needed in the body and setting up polling to wait for it to load 🙄. It seems that the distinction on the async=“” which turns into async=“async” is what’s causing the double render, but there’s no way to patch that easily yet.

Loading the script in the following Head component works for me so far. Fingers crossed.

const NonSSRHead = dynamic(
    () => import('next/head'),
    { ssr: false }
);

I'm using this workaround now. Though I still get a console warning Warning: Expected server HTML to contain a matching <header> in <div>. from time to time locally after hot reload. It then seems to break some styles. A page reload fixes it.

I changed some code and this stopped working. I am now using react-script-loader-hoc (https://github.com/sesilio/react-script-loader-hoc), which is based on this: https://usehooks.com/useScript/
Works so far, no more console errors about

You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.

Just came across this too, PR to hopefully fix ☝️

Was this page helpful?
0 / 5 - 0 ratings

Related issues

flybayer picture flybayer  ·  3Comments

YarivGilad picture YarivGilad  ·  3Comments

jesselee34 picture jesselee34  ·  3Comments

kenji4569 picture kenji4569  ·  3Comments

havefive picture havefive  ·  3Comments