menu

Nuxt Third Party Code Is Poison: Cure It In 3 Easy Steps

We've all seen it before. You just launched a new, minty-fresh, fully optimized, Web 4.0 Nuxt site. Everyone is ecstatic. All of your team's hard work has finally paid off.

Then, the third party code starts creeping in.

It starts out small. Just a new event here, a tracking pixel there. No big deal, right? Then a contract gets finalized and you have 3 days to add a chat provider. "Ug, this is hacky as hell" you think, "I'll rework it when I have time".

Then your lead developer quits and you have to pick up their product reviews integration. "Holy crap, I'm just gonna shove this right on the product page for now". After the dust settles, you find yourself knee deep in a giant pile of technical debt that's a tangled mess of half measures and band-aids.

Been there, done that. With a little bit of planning and forethought you can increase your chances of survival. The following three steps are proven ways to take control over your external integrations. They tackle all the different ways other people's code reek havoc on your codebase. Enjoy.

#1 Move All External Integrations To Plugins

The standard way to add scripts to Nuxt sites is to use head(). This may be ok for a script or two, but this is where the poison slowly starts to leak in.

layouts/default.vue
head() {
  return {
    script:[{ src: 'https://example.com/chat.js' }]
  }
}

Maybe you added your scripts to your config. At least it's out of your main code, that's clean right?

nuxt.config.js
head: {
  __dangerouslyDisableSanitizersByTagID: {
    'tracker': ['innerHTML']
  },
  script:[
    { src: 'https://example.com/tracker.js', defer: true },
    { hid: 'tracker', innerHTML: 'initTracker();' }
  ]
}

The only clean way to get external dependencies out of your main code is to move them to plugins. This requires a little extra work but it's well worth it.

plugins/tracker.client.js
export default() => {
  // manually add the script to the DOM
  const script = document.createElement('script')
  script.src = 'https://example.com/tracker.js'
  document.head.appendChild(script)

  // call whatever js the external script needs.
  window.initTracker();
}

Benefits

  • Focused - By using a single integration per file it keeps the code super focused and much easier to manage.
  • Natural - Since it's already in JavaScript you can use the full power of your IDE and it becomes easier to decipher vs vue-meta's conventions and constraints.
  • Flexible - Moving the code into a plugin gives you the flexibility you need to optimize integrations much easier.

But What About The Nuxt Community Module XYZ?

Look, there are plenty of decent Nuxt modules that help with this issue somewhat. Each has their pros and cons. Choose the route that makes sense for you.

In general, I rarely use Nuxt Community Modules because I either outgrow them or they force more junk into the nuxt.config.js file. I guess you could say I'm somewhat of a purist. To me, the config file is for configuring my app, not the 3rd party poison I'm trying to inject into the client's browser.

#2 Auto-Inject Your Integration Plugins

If you followed Step 1, you can take it one step further and auto-inject them into your application!

Create a folder called integrations and add this module to your project. Now, any file you create in that folder will automatically be injected into your app as a plugin.

modules/integrations.js
import { resolve, join } from "path";
import { readdirSync, statSync } from "fs";

export default function () {
  this.nuxt.hook("build:before", () => {
    const folder = resolve(__dirname, "../integrations");
    const files = readdirSync(folder);

    files.forEach((file) => {
      const filename = resolve(folder, file);
      const stat = statSync(filename);

      if (stat.isFile()) {
        const { dst } = this.addTemplate(filename);
        this.options.plugins.push(join(this.options.buildDir, dst));
      }
    });
  });
}
integrations/example.client.js
export default () => {
  console.log("example integration.  hello world!!");
};

Now, add this module to your nuxt.config.js

nuxt.config.js
module.exports = {
  mode: 'universal',
  modules: ['~/modules/integrations.js']
}

Benefits

  • Less boilerplate to add new integrations
  • Less Nuxt config bloat
  • Easily extendable for multi-site generation

#3 Make Them Pay Wait

Now that you have your integrations in auto-injected plugins, now is the time to optimize. By default you should make everything as lazy as possible. At the bare minimum everything should wait until at least the Page Load event.

The goal here is to let your app load before the onslaught of third party code is unleashed (which can be many times larger than your own app!)

Below are some simple utility classes for loading external JavaScript lazily. I've stripped them down a bit to make them easy to use and learn. Don't be afraid to extend them and make them your own!

🐢 The Turtle: Loads a script after page load and runs a callback

helpers/onLoad.js
// callback - the function to run after onLoad
// delay - wait X milliseconds after onLoad
export const onLoad = (callback, delay = 1) => {
  // missed the load event, run now
  if (document.readyState === "complete") {
    setTimeout(() => callback(), delay);
  } else {
    window.addEventListener("load", function() {
      setTimeout(() => callback(), delay);
    });
  }
};
integrations/myLazyVendor.js
import { onLoad } from "../helpers/onLoad";

export default () => {
  onLoad(() => {
    console.log("do something after onLoad");
  });
};

🐌 The Snail: Only loads on demand, but STILL after page load

helpers/onDemand.js
export class onDemand {
  // src - the full url to load
  // waitForPageLoad - still wait for the page load event on first hit
  constructor(src, waitForPageLoad = true) {
    this.isLoaded = false;
    this.isLoading = false;
    this.callbacks = [];
    this.src = src;
    this.waitForPageLoad = waitForPageLoad;
  }
  load(callback = () => {}) {
    if (this.isLoaded) return callback();

    this.callbacks.push(callback);

    if (!this.isLoading) {
      this.isLoading = true;
      if (!this.waitForPageLoad || document.readyState === "complete")
        this._loadScript();
      else window.addEventListener("load", () => this._loadScript());
    }
  }

  _loadScript() {
    const script = document.createElement("script");
    script.src = this.src;
    script.onload = () => this._invokeCallbacks();
    document.getElementsByTagName("head")[0].appendChild(script);
  }

  _invokeCallbacks() {
    this.isLoaded = true;
    this.callbacks.forEach(callback => callback());
  }
}

Here we create a Google Maps plugin to separate all of the 3rd party loading logic and window calls.

integrations/googleMaps.client.js

import { onDemand } from "../helpers/onDemand";

export default (ctx, inject) => {
  const loader = new onDemand(
    "https://maps.googleapis.com/maps/api/js?key=xxx&libraries=places"
  );

  inject("places", {
    plot(el, lat, lng) {
      loader.load(() => {
        new window.google.maps.Map(el, {
          center: { lat, lng },
          zoom: 17
        });
      });
    }
  });
};

Here is a map component that uses the Google Places plugin. Look how thin that sucker is, that's your overall goal. Now the component can be pure and proxy the needed functionality through the plugin.

components/map.vue
<template>
  <div>
    <div style="height:400px;width:400px;" ref="map"></div>
  </div>
</template>

<script>
export default {
  mounted() {
    this.$places.plot(this.$refs.map, 46.8772, -96.7898)
  },
}
</script>

🙈 The Monkey: Loads just before it's visible.

By using the Vue Observe Visibility Library we can delay our map even more! Now we only load it just before it's visible.

plugins/observe.client.js
import Vue from 'vue'
import VueObserveVisibility from 'vue-observe-visibility'

Vue.use(VueObserveVisibility)
nuxt.config.js
plugins:['./plugins/observe.client.js']
components/map.vue
<template>
  <div>
    <div style="height:400px;width:400px;" v-observe-visibility="plot" ref="map"></div>
  </div>
</template>

<script>
export default {
  methods: {
    plot(isVisible) {
      if (isVisible) this.$places.plot(this.$refs.map, 46.8772, -96.7898)
    },
  },
}
</script>

Benefits

  • Performance - Better start render, time to interactive, and lighthouse scores
  • SEO - Better search engine rankings
  • Fame - Get a high-five from your boss

Not sure when to use which? Here is a quick breakdown:

  • Needed on all pages? Use Turtle.
  • Used on only a couple pages? Use Snail
  • Is heavy and below the fold? Use Monkey

Again, the goal should be to make external integrations as lazy as you can tolerate without negatively affecting the user experience.

Summary

What are you waiting for?! If you follow these 3 simple steps, I guarantee you'll be much happier managing your third party integrations. Not only will you be more productive onboarding new ones, but updating and swapping existing ones will be a breeze.

That's all for now, take care!