parcel-bundler / parcel

The zero configuration build tool for the web. 📦🚀
https://parceljs.org
MIT License
43.39k stars 2.26k forks source link

Parcel 2: Asset Preloading and Prefetching #3757

Open devongovett opened 4 years ago

devongovett commented 4 years ago

Modern browsers support Resource Hints, which allow declaratively specifying resources that a web page will need in the future. This is done by inserting <link> tags in the document head, which allows browsers to discover these resources before they normally would and prioritize downloading and caching them, thus speeding up page loads and subsequent navigations. For example, resources like web fonts and background images referenced in a CSS file could be preloaded rather than waiting for the CSS file to be loaded and parsed to start downloading them. Code split bundles could also be prefetched at a lower priority so that subsequent user actions that would depend on asynchronously loaded code could be ready and potentially even parsed in the background ahead of when they are needed.

Parcel has a graph of all of the resources in an application at build time, so it is the perfect tool to automatically insert these <link> tags.

Preloading

<link rel="preload"> can be used to speed up page loads by hoisting downloads of resources that would normally be discovered after loading something else. For example, the browser normally needs to download and parse CSS in order to discover background images and fonts. By hoisting these into the HTML, the browser can download those resources in parallel instead. This could be done automatically by Parcel at build time.

In the HTML packager, Parcel already inserts <script> and <link rel="stylesheet"> tags for each of the directly referenced bundles. We should also insert <link rel="preload"> tags for each resource that is referenced by these bundles that would be immediately loaded, e.g. fonts, images, etc.

In the JS runtime, we should do the same thing. We already load CSS in parallel with JS when loading a code split bundle, but we should also load images and other resources referenced in that CSS/JS. This can be done by dynamically inserting <link rel="preload"> tags into the HTML at runtime.

This will require some bundling changes. Currently, those resources are marked as "async" so are placed in their own bundle group. Ideally, they would be placed in the same bundle group as the bundle that referenced them. This way, the JS runtime and HTML packager can pick up those resources and preload them the same way they already handle JS and CSS.

We could also handle responsive preloading by automatically adding the media attribute to the <link> tag based on the @media rule a resource was nested inside of in CSS. This would avoid preloading media that would not be used on the user's device.

One concern is how to detect whether resources are used immediately or not. For example, a CSS class with a background image may not be applied immediately. Chrome logs warnings in the console when a preloaded resource isn't used within a few seconds of page load. It could be argued that loading CSS that isn't used right away is also bad, but either way, there's no way to know at build time whether a resource will be used immediately. Perhaps these should be prefetches rather than preloads? Or maybe we need a syntax in CSS/JS to specify which resources should be preloaded and which should be prefetched? Feedback and ideas wanted here.

Prefetching

<link rel="prefetch"> can be used to speed up subsequent navigations within an application by prefetching resources in the background while the browser is idle. This means that those downloads won't block higher priority resources such as those for the current page, but hopefully by the time the user navigates to a subsequent part of the app many resources are already loaded. These <link> tags can be embedded into the HTML, or added by JavaScript on demand.

Parcel should allow users and frameworks to trigger prefetching of the resources that would be loaded for a code split bundle. Dynamic import() normally downloads and immediately executes a bundle and it's dependencies, but prefetching would only download and cache the resource without immediately executing it.

One way to do this is to support a runtime API to trigger prefetching. Parcel has all of the metadata about what bundles to load already, so we just need to expose this information. For example, on mouse over on a button, prefetching could be triggered so that the browser has a head start when the user decides to click it.

import prefetch from '@parcel/prefetch';

async function onMouseOver() {
  prefetch('./other');
}

async function onClick() {
  await import('./other');
}

This could be statically analyzed at build time to generate the necessary prefetching code.

Feedback

Please leave feedback in the comments below. It would be really useful to hear your usecases and ideas for this.

wojtekmaj commented 4 years ago

I think it would be nice to have magic comments like in Webpack, for the sake of simplicity of migration from one solution to another.

import(/* parcelPrefetch: true */ 'LoginModal');
devongovett commented 4 years ago

I don't think magic comments are a great idea personally. Plus, if the comment is parcelPrefetch it already isn't compatible with webpack anyway.

theKashey commented 4 years ago

magic comment is not a bad idea for a static prefetch, however I am not sure that static prefetch is actually needed - there is a reason to preload some scripts with a chunk, but prefetch? I would personally prefetch as a command I can call at any time.

linking webpack issue - https://github.com/webpack/webpack/issues/8470

kim366 commented 3 years ago

What's the status on this or what's a viable workaround for prefetching/preloading right now?

mischnic commented 3 years ago

static prefetching and preloading is implemented. We should actually document this

https://github.com/parcel-bundler/parcel/blob/v2/packages/core/integration-tests/test/integration/dynamic-static-prefetch/async.js

https://github.com/parcel-bundler/parcel/blob/v2/packages/core/integration-tests/test/integration/dynamic-static-preload/async.js

https://github.com/parcel-bundler/parcel/pull/5158

kim366 commented 3 years ago

Oh neat! Yeah, whenever I search for this feature, this very issue is the only reasonable result that comes up, so I guess it is now documented :)

PS: this issue should probably be closed

mischnic commented 3 years ago

https://github.com/parcel-bundler/parcel/blob/v2/packages/core/integration-tests/test/integration/dynamic-static-prefetch/.babelrc

damianobarbati commented 3 years ago

@mischnic do you have an example on how to use the renamed babel plugin @babel/plugin-syntax-import-assertions? Simply installing it and renaming the plugin in your configuration is not working. Using:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false,
                "useBuiltIns": "usage",
                "debug": false,
                "corejs": 3
            }
        ],
        "@babel/preset-react"
    ],
    "plugins": [
        [
            "@babel/plugin-syntax-import-assertions",
            {
                "version": "may-2020"
            }
        ]
    ]
}

and

const Auth = lazy(() => import("./components/Auth.js", { preload: true }));

breaks the parser as well ☹️

mischnic commented 3 years ago

What error do you get? (And what Parcel version are you on)

damianobarbati commented 3 years ago

@mischnic false allarm: I had installed parcel-bundler@1 instead of parcel@^2.0.0-beta.2!

I'm migrating all repos to parcel v2 but I keep loosing pieces around... sorry for the noise and thanks for helping out.

EDIT: with preload=true the js module supposed to be loaded asap, correct? Without waiting for explicit fetch.

import React, { Suspense, lazy } from "react";
import { createBrowserHistory } from "history";
import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom";
import Layout from "./components/Layout.js";

const Home = lazy(() => import("./components/Home.js", { preload: true }));
const APIDocs = lazy(() => import("./components/APIDocs.js", { preload: true }));

const Router = () => {
    return (
        <BrowserRouter>
            <Layout>
                <Suspense fallback={<Spinner />}>
                    <Switch>
                        <Route path={"/"} exact={true} component={Home} />
                        <Route path={"/api-docs"} exact={true} component={APIDocs} />
                    </Switch>
                </Suspense>
            </Layout>
        </BrowserRouter>
    );
};

export default Router;

I was expecting with this code, to see in network Home.<hash>.js and APIDocs.<hash>.js loaded as soon as index.js is run. But I see only Home.js loaded, and APIDocs.js on explicit route change.

dioptre commented 2 years ago

Is there an example of how to use this with images?

ChristophP commented 2 years ago

I think most of the comments in this thread are from over a year ago. Is there any news about parcel V2 and the current prefetching status?

Jaysolutely commented 2 years ago

@devongovett Hi, I would highly welcome your proposal exactly as you outlined. My usecase is a SPA in which the first displayed page should be preloaded from an inline script inside the index.html. This way the page can be displayed instantly when the main application calls the dynamic import. (Doing this directly with import() results in dependency problems due to its asynchronous nature.) Thank you for your work.