Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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:
- Host: GitHub
- URL: https://github.com/vhscom/fetch-inject
- Owner: vhscom
- License: zlib
- Created: 2022-02-19T05:45:48.000Z (over 2 years ago)
- Default Branch: trunk
- Last Pushed: 2024-05-26T03:58:53.000Z (6 months ago)
- Last Synced: 2024-06-25T19:00:34.874Z (5 months ago)
- Topics: fetch-injection, javascript, node-module, nodejs, webperf
- Language: HTML
- Homepage: https://codeberg.org/vhs/fetch-inject
- Size: 2.8 MB
- Stars: 12
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: COPYING
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.
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 typeUSVString
orRequest
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('