How to remove client-side JavaScript from Gatsby

I build my reviews site using Gatsby. It's cool, the graphql is powerful but there's something that was buggering me.

Why does my static site need client-side JavaScript?

There's no reason for it. Gatsby will argue about faster route changing this way, sure. For a high-traffic site where users visit multiple pages on the same visit, it makes sense. Not my case.

Astonishingly Gatsby does not provide an out-of-the-box setting or flag to amend this issue. I would've loved for a boolean in the config static if I need JS. To it builds a a truly static site, which can be done with Jekyll, Hugo or Eleventy.

No JavaScript plugin

I found this plugin but it doesn't seem to work with sites under a subfolder such as example.com/mysite

Then I encountered this Gist with the small amendment needed so it works in a subdirectory installation. It's based on the plugin but you don't need the plugin installed to work.

Create a gatsby-ssr.js file in the root directory of your site. it will remove all JavaScript from the final build (not when running the develop task).

⚠️ It did not fully work in my Gatsby 2.25.3, it needed a small optional chaining around the polyfill condition.

Here's my updated version:

let pageScripts
/*
 * The "scripts" variable is not documented by Gatsby, https://www.gatsbyjs.org/docs/ssr-apis/#onRenderBody, and that is probably for a good
 * reason. The variable contains the scripts the Gatsby internals, https://github.com/gatsbyjs/gatsby/blob/d9cf5a21403c474846ebdf7a0508902b9b8a2ea9/packages/gatsby/cache-dir/static-entry.js#L270-L283,
 * puts into the head and post body. We will be relying on this undocumented variable until it does not work anymore as the alternative is
 * to read the webpack.stats.json file and parse it ourselves.
 */
export function onRenderBody({ scripts }) {
  if (process.env.NODE_ENV !== "production") {
    // During a gatsby development build (gatsby develop) we do nothing.
    return
  }
  // TODO maybe we should not even wait and see if Gatsby removes this internal "script" variable and code around the issue if the variable is not there.
  if (!scripts) {
    throw new Error(
      "gatsby-plugin-no-javascript: Gatsby removed an internal detail that this plugin relied upon, please submit this issue to https://www.github.com/itmayziii/gatsby-plugin-no-javascript."
    )
  }
  pageScripts = scripts
}
// Here we rely on the fact that onPreRenderHTML is called after onRenderBody so we have access to the scripts Gatsby inserted into the HTML.
export function onPreRenderHTML(
  {
    getHeadComponents,
    pathname,
    replaceHeadComponents,
    getPostBodyComponents,
    replacePostBodyComponents,
  },
  pluginOptions
) {
  if (
    process.env.NODE_ENV !== "production" ||
    checkPathExclusion(pathname, pluginOptions)
  ) {
    // During a gatsby development build (gatsby develop) we do nothing.
    return
  }
  replaceHeadComponents(
    getHeadComponentsNoJS(getHeadComponents(), pluginOptions)
  )
  replacePostBodyComponents(
    getPostBodyComponentsNoJS(getPostBodyComponents(), pluginOptions)
  )
}

function getHeadComponentsNoJS(headComponents, pluginOptions) {
  return headComponents.filter(headComponent => {
    // Not a react component and therefore not a <script>.
    if (!isReactElement(headComponent)) {
      return true
    }

    if (
      pluginOptions.excludeFiles &&
      headComponent.props.href &&
      RegExp(pluginOptions.excludeFiles).test(headComponent.props.href)
    ) {
      return true
    }
    // Gatsby puts JSON files in the head that should also be removed if javascript is removed, all these Gatsby files are in the
    // "/static" or /page-data directories.
    if (
      headComponent.props.href &&
      (headComponent.props.href.startsWith(`${__PATH_PREFIX__}/static/`) ||
        headComponent.props.href.startsWith(`${__PATH_PREFIX__}/page-data/`))
    ) {
      return false
    }

    return (
      pageScripts.find(script => {
        return (
          headComponent.props.as === "script" &&
          `${__PATH_PREFIX__}/${script.name}` === headComponent.props.href &&
          script.rel === headComponent.props.rel
        )
      }) === undefined
    )
  })
}
function getPostBodyComponentsNoJS(postBodyComponents, pluginOptions) {
  return postBodyComponents.filter(postBodyComponent => {
    // Not a react component and therefore not a <script>.
    if (!isReactElement(postBodyComponent)) {
      return true
    }
    if (
      pluginOptions.excludeFiles &&
      postBodyComponent.props.src &&
      RegExp(pluginOptions.excludeFiles).test(postBodyComponent.props.src)
    ) {
      return true
    }
    // These are special Gatsby files we are calling out specifically.
    if (
      postBodyComponent.props.id &&
      (postBodyComponent.props.id === "gatsby-script-loader" ||
        postBodyComponent.props.id === "gatsby-chunk-mapping")
    ) {
      return false
    }

    if (postBodyComponent.props.src?.indexOf("polyfill") > -1) {
      return false
    }

    return (
      pageScripts.find(script => {
        return (
          postBodyComponent.type === "script" &&
          `${__PATH_PREFIX__}/${script.name}` === postBodyComponent.props.src
        )
      }) === undefined
    )
  })
}
function isReactElement(reactNode) {
  return reactNode.props !== undefined
}
function checkPathExclusion(pathname, pluginOptions) {
  if (!pluginOptions.excludePaths) return false
  return RegExp(pluginOptions.excludePaths).test(pathname)
}
Code language: JavaScript (javascript)

Performance

  • Reduced from about 35 requests to less than 10.
  • The site finishes loading in about 0.7 seconds comparted to 1.5 seconds when using the JavaScript (and JSON) files.

Leave a Reply

Your email address will not be published. Required fields are marked *