nuxt / scripts

Plug-and-play script optimization for Nuxt applications. (Public Preview)
https://scripts.nuxt.com
MIT License
126 stars 8 forks source link

Third Party Capital Discussion #54

Open harlan-zw opened 1 month ago

harlan-zw commented 1 month ago

Background

Third-party capital was being used previously to generate some of the static data needed for a subset of the registry scripts (youtube, google maps, google analytics, google tag manager).

The runtime dependency has since been removed in a PR for several reasons below. We still use the exported types for the scripts.

Runtime Overhead

Third-party-capital has quite a hefty runtime bundle size relatively. Consider that the useScript wrapper from Unhead and Nuxt itself is only around ~2kb, which I think is too high and would be exploring further ways to bring it down. For Nuxt Scripts to be maximally useful it needs to be as minimal as possible.

The minified bundled size for each export with comparisons for current size:

Convulated implementation / no implementation types

TPC abstracts away the implementation details inside of a JSON file that has custom parsing, this makes it quite difficult to debug the behaviour of the code without digging into the entire code base implementation.

import { GoogleAnalytics } from 'third-party-capital'

const schema = GoogleAnalytics({ /* options */)
// schema is generically typed
const scripts = schema.scripts 
// scripts is an array, no way to know which script is the load of the script verse the bootstrap setup

Because the implementation is not obvious, it adds a disconnect between how the the use() and the stubs are implemented versus the rest of the script.

For example for SSR in GTM, we need to stub dataLayer as an array so we can push to it.

   scriptOptions: {
      use() {
        return { dataLayer: window.dataLayer, google_tag_manager: window.google_tag_manager }
      },
      // allow dataLayer to be accessed on the server
      stub: import.meta.client
        ? undefined
        : ({ fn }) => {
            return fn === 'dataLayer' ? [] : undefined
          },
    },

Leveraging Nuxt Tree Shaking for Micro-Optimizations

Since Nuxt is a SSR framework, there are certain optimizations we can opt-in to that environment agnostic packages can't, as they have no control over the SSR lifecycle.

if (import.meta.server) {
  // offload logic to the server instead of having the client do it
  // - gets treeshaken out of the client-build
}

A simple example of a micro-optimization is not passing any code that only the client needs to use. For example, we need to bootstrap the window for most third-party scripts, in Nuxt we can easily leverage tree-shaking to make sure this code isn't in the server bundle.

// packages publish code like this to be environment agnostic
if (typeof window !== 'undefined') {

}
// in nuxt we can just treeshake as we have control over rollup
if (import.meta.client) {
  window.myLib = {}
}

For something concrete, we can consider not running head injection tasks on the client when we can just do it on the server, reducing the time to hydrate the client.

We can see this in the ScriptYouTubePlayer component.

if (import.meta.server) {
  useHead({
    link: [
      {
        rel: props.aboveTheFold ? 'preconnect' : 'dns-prefetch',
        href: 'https://i.ytimg.com',
      },
      props.aboveTheFold
        // we can preload the placeholder image
        ? {
            rel: 'preload',
            as: 'image',
            href: placeholder.value,
          }
        : {},
    ],
  })
}

To achieve this with TPC would be quite difficult and would unlikely to work even if we can tree shake.

Maintanence DX

Nuxt Scripts is providing out-of-the-box support for 16 scripts, having 4 of them being configured differently through an external dependency we don't have control over will make maintenance harder in the short and long term.

I'd also like to see the repo more active generally, the following remain open and unanswered.

Reimplementing TPC

I'm happy to reconsider adding TPC within the runtime if the above can be solved or some other value can be provided that justifies the constraints.

huang-julien commented 1 month ago

I think the way of integrating TPC into nuxt-scripts is to do it at build-time. Not at nuxt-script bundle-time but at users build-time by having TPC as a peerdep and external within nuxt scripts.

The idea is to have TPC kept as external but still added in the registry. It will be up to users to update TPC to have the latest version. We could also check TPC version within nuxt-script module setup time and warn the user that a new version of TPC is available.