Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/vhscom/fetch-inject

🍒 Dynamically inline assets into the DOM using Fetch Injection. Mirror of Fetch Inject on Codeberg. :point_down:
https://github.com/vhscom/fetch-inject

fetch-injection javascript node-module nodejs webperf

Last synced: 3 months ago
JSON representation

🍒 Dynamically inline assets into the DOM using Fetch Injection. Mirror of Fetch Inject on Codeberg. :point_down:

Awesome Lists containing this project

README

        

# Fetch Inject

[![Latest NPM version](https://flat.badgen.net/npm/v/fetch-inject)](https://npmjs.com/fetch-inject)
[![Weekly downloads](https://flat.badgen.net/npm/dw/fetch-inject)](https://npmjs.com/fetch-inject)
[![Compressed size](https://flat.badgen.net/bundlephobia/minzip/fetch-inject)](https://bundlephobia.com/package/fetch-inject)
[![Hits per month](https://flat.badgen.net/jsdelivr/hits/npm/fetch-inject)](https://www.jsdelivr.com/package/npm/fetch-inject)
[![License](https://flat.badgen.net/npm/license/fetch-inject)](https://codeberg.org/vhs/fetch-inject)

Dynamically inline assets into the DOM using [Fetch Injection](https://vhs.codeberg.page/post/managing-async-dependencies-javascript/).

Read the [**Hacker News discussion**](https://news.ycombinator.com/item?id=14380191).

## Overview 🌱

Fetch Inject implements a performance optimization technique called [Fetch Injection](https://vhs.codeberg.page/post/managing-async-dependencies-javascript/) for managing asynchronous JavaScript dependencies. It works for stylesheets too, and was designed to be extensible for any resource type that can be loaded using [`fetch`](https://devdocs.io/dom-fetch/).

Use Fetch Inject to dynamically import external resources in parallel (even across the network), and load them into your page in a desired sequence, at a desired time and under desirable runtime conditions.

Because it uses the [Fetch API](http://devdocs.io/dom/fetch_api) Fetch Inject works alongside [Service Workers](http://devdocs.io/dom-service-workers/) for creating offline-first, installable progressive web applications and saves bandwidth on metered networks.

## Playground 🛝

Try [CodePen Playground](https://codepen.io/vhsdev/pen/QWOoJqG?editors=0012) while referencing the [Use Cases](#use-cases-) provided below.

## Performance ⚡️

The two following network waterfall diagrams were produced using Fetch Inject to load the WordPress Twenty Seventeen theme for a performance talk given at WordCamp Ubud 2017. Stats captured over a 4G network using a mobile Wi-Fi hotspot. The left-hand side shows page speed with an unprimed browser cache and the other using Service Worker caching.


Screenshot of network waterfall showing parallel resource loading using Fetch Inject
Screenshot of network waterfall showing parallel resource loading using Fetch Inject with Service Workers

Notice with Service Workers (right) most latency occurs waiting for the initial response.

## Installation 💾

```sh
bun i fetch-inject # or pnpm, yarn, npm, etc.
```

Or import a version directly from CDN like:

```html

import fetchInject from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

```

Ships ESM by default. For asset pipelines requiring UMD, AMD or CJS check out version 2 and below.

## Usage 🌀

Promise fetchInject( inputs[, promise[, options]] )

### Parameters


inputs

Resources to fetch. Must be an Array of type USVString or Request objects.

promise

Optional. Promise to await before injecting fetched resources.

options

Optional. Enables customization of Fetch implementation used.

### Return value

A [`Promise`](http://devdocs.io/javascript/global_objects/promise) that resolves to an `Array` of `Object`s. Each `Object` contains a list of resolved properties of the [`Response`](http://devdocs.io/dom/response) [`Body`](http://devdocs.io/dom/body) used by the module, e.g.

```js
[
{
blob: { size: 44735, type: 'application/javascript' },
text: '/*!↵ * Bootstrap v4.0.0-alpha.5 ... */'
},
{
blob: { size: 31000, type: 'text/css' },
text: '/*!↵ * Font Awesome 4.7.0 ... */'
}
];
```

## Use Cases 🎯

[Use the Playground](#playground-) to try any of these on your own.

### Preventing Script Blocking

**Problem:**
External scripts can lead to [jank](http://jankfree.org/) or [SPOF](https://www.stevesouders.com/blog/2010/06/01/frontend-spof/) if not handled correctly.

**Solution:**
Load external scripts [without blocking](https://www.stevesouders.com/blog/2009/04/27/loading-scripts-without-blocking/):

```js
fetchInject(['https://cdn.jsdelivr.net/npm/flexsearch/dist/flexsearch.bundle.min.js']);
```

This is a simple case to get you started. Don't worry, it gets better.

### Loading Non-critical CSS

**Problem:**
[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) and [Lighthouse](https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk) ding you for loading unnecessary styles on initial render.

**Solution:**
Inline your critical CSS and load [non-critical styles](https://gist.github.com/scottjehl/87176715419617ae6994) asynchronously:

```html

/*! bulma.io v0.4.0 ... */

import fetchInject from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
fetchInject([
'/css/non-critical.css',
'https://cdn.jsdelivr.net/fontawesome/4.7.0/css/font-awesome.min.css'
]);

```

See also [Suspense](#suspense) below.

### Lazyloading Scripts

**Problem:**
You want to load a script in response to a user interaction without affecting your page load times.

**Solution:**
Create an event listener, respond to the event and then destroy the listener.

```js
const el = document.querySelector('details summary');
el.onclick = (evt) => {
fetchInject(['https://cdn.jsdelivr.net/smooth-scroll/10.2.1/smooth-scroll.min.js']);
el.onclick = null;
};
```

Here we are loading the smooth scroll polyfill when a user opens a [details](http://devdocs.io/html/element/details) element, useful for displaying a collapsed and keyboard-friendly table of contents.

### Responding to Asynchronous Scripts

**Problem:**
You need to perform a synchronous operation immediately after an asynchronous script is loaded.

**Solution:**
You could create a `script` element and use the [`async`](http://devdocs.io/html/attributes#async-attribute) and `onload` attributes. Or you could...

```js
fetchInject(['https://cdn.jsdelivr.net/momentjs/2.17.1/moment.min.js']).then(() => {
console.log(`Finish in less than ${moment().endOf('year').fromNow(true)}`);
});
```

### Ordering Script Dependencies

**Problem:**
You have several scripts that depend on one another and you want to load them all asynchronously, in parallel, without causing race conditions.

**Solution:**
Pass `fetchInject` to itself as a second argument, forming a promise recursion:

```js
fetchInject(
['https://npmcdn.com/[email protected]/dist/js/bootstrap.min.js'],
fetchInject([
'https://cdn.jsdelivr.net/jquery/3.1.1/jquery.slim.min.js',
'https://npmcdn.com/[email protected]/dist/js/tether.min.js'
])
);
```

### Managing Asynchronous Dependencies

**Problem:**
You want to load some dependencies which require some dependencies, which require some dependencies. You want it all in parallel, and you want it now.

**Solution:**
You could scatter some `link`s into your document head, blocking initial page render, bloat your application bundle with scripts the user might not actually need. Or you could...

```js
const tether = ['https://cdn.jsdelivr.net/tether/1.4.0/tether.min.js'];
const drop = ['https://cdn.jsdelivr.net/drop/1.4.2/js/drop.min.js'];
const tooltip = [
'https://cdn.jsdelivr.net/tooltip/1.2.0/tooltip.min.js',
'https://cdn.jsdelivr.net/tooltip/1.2.0/tooltip-theme-arrows.css'
];
fetchInject(tooltip, fetchInject(drop, fetchInject(tether))).then(() => {
new Tooltip({
target: document.querySelector('h1'),
content: 'You moused over the first H1!',
classes: 'tooltip-theme-arrows',
position: 'bottom center'
});
});
```

What about jQuery dropdown menus? Sure why not...

```js
fetchInject(
['/assets/js/main.js'],
fetchInject(
['/assets/js/vendor/superfish.min.js'],
fetchInject(
['/assets/js/vendor/jquery.transit.min.js', '/assets/js/vendor/jquery.hoverIntent.js'],
fetchInject(['/assets/js/vendor/jquery.min.js'])
)
)
);
```

### Loading and Handling Composite Libraries

**Problem:**
You want to deep link to gallery images using [PhotoSwipe](http://photoswipe.com/) without slowing down your page.

**Solution:**
Download everything in parallel and instantiate when finished:

```js
const container = document.querySelector('.pswp')
const items = JSON.parse({{ .Params.gallery.main | jsonify }})
fetchInject([
'/css/photoswipe.css',
'/css/default-skin/default-skin.css',
'/js/photoswipe.min.js',
'/js/photoswipe-ui-default.min.js'
]).then(() => {
const gallery = new PhotoSwipe(container, PhotoSwipeUI_Default, items)
gallery.init()
})
```

This example turns TOML into JSON, parses the object, downloads all of the PhotoSwipe goodies and then activates the PhotoSwipe gallery immediately when the interface is ready to be displayed.

### Suspense

**Problem:** You're experiencing a flash of unstyled content when lazy-loading page resources.

**Solution:** Hide the content until your styles are ready:

```js
const pageReady = new Promise((resolve, reject) => {
document.onreadystatechange = () => {
document.readyState === 'complete' && resolve(document);
};
});

fetchInject(
['https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'],
pageReady
).then(() => (document.body.style.visibility = 'visible'));
```

### A/B Testing

**Problem:** You want to run A/B tests and track the winning experience.

**Solution:** Dynamically import Fetch Inject, and display and report the winning experience.

```html

localStorage.setItem('experience', ['test-a.css', 'test-a.js']);

const experience = localStorage.getItem('experience');
const analytics = 'analytics.min.js';

import('https://cdn.jsdelivr.net/npm/fetch-inject')
.then((module) => {
console.time('page-time');
module.default(experience.split(',').concat(analytics))
.then((injections) => {
analytics.track({
testGroup: experience, // "test-a.css,test-a.js"
waitTime: console.timeLog('page-time') // "0.231953125 ms"
extraData: injections // [ Blob {size: 7889, ...} ]
});
document.onclose = _ => analytics.track({
testGroup: experience,
pageTime: console.endTime('page-time') // "8.29296875 ms"
})
})
.catch((errors) => analytics.error(errors))
});

```

Use alongside [Suspense](#suspense) for a winning combination.

### Web Components

Version 3.3.1 introduces support for HTML lt;template> element Fetch Injection, making it possible to build web components using using HTML templates without a build step. To use it simply create an HTML file like:

```html


Prompt:

75 of 75 tokens remaining

```

Define your custom element in a JS file:

```js
customElements.define(
'pico-prompt',
class extends HTMLElement {
static observedAttributes = ['placeholder', 'value', 'disabled'];
constructor() {
super()
.attachShadow({ mode: 'open' })
.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
/* snip */
}
);
```

Ensure HTML templates are injected before registering your custom elements:

```html

import fetchInject from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
fetchInject(
['./components/pico-dialog.js', './components/pico-prompt.js', './components/pico-fab.js'],
fetchInject([
'./components/pico-dialog.html',
'./components/pico-prompt.html',
'./components/pico-fab.html'
]).then(() => {
document.body.style.visibility = 'visible';
})
);

```

Combine with [Suspense](#suspense) to avoid FOUC.

## SvelteKit

As of version 3.1.0 Fetch Inject supports use with SvelteKit. Use it inside your `load` functions to run Fetch requests client- and server-side. Or drop it inside your [hooks](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) in order to inject resources into the document like so:

```js
export const handle: Handle = sequence(
async ({ event, resolve }) => {
const response = await resolve(event);
const [flexsearch] = await fetchInject([
'https://cdn.jsdelivr.net/npm/flexsearch/dist/flexsearch.light.min.js'
]);

const body = await response.text();
return new Response(
body.replace('