jantimon / html-webpack-plugin

Simplifies creation of HTML files to serve your webpack bundles
MIT License
10.71k stars 1.31k forks source link

Add a link rel=prefetch for a css chunks created within your wepbackPrefetch lazy loaded js chunks #1832

Closed StadnykYura closed 6 months ago

StadnykYura commented 11 months ago

Is your feature request related to a problem? Please describe. When using a webpackPrefetch hint on a dynamicly imported js module the webpack does not have a support for dynamic runtime injection of related lazy css chunk (extracted with MiniCssExtractPlugin) to the lazy loaded js chunk.

Current behaviour Currently, as @sokra mentioned https://github.com/jantimon/html-webpack-plugin/issues/1317#issuecomment-704870353, webpackPrefetch hint on js modules automatically adds something like this <link rel="prefetch" as="script" href="http://host:port/lazy-component.js"> to the html head after the initial chunk evaluation.

There is no need to html-webpack-plugin to add prefetch tags. webpack already adds at runtime and that's not too late as they are intended to download after the other files.

And if you are using the style-loader in a combination with css-loader and sass-loader everything is fine, bcs css included into js, the js is prefetched during runtime on browser idle time. So later, when the js module is used/rendered, the js chunk is fetched from the prefetch cache, and no additional request for other resources is done.

The Problem?/Issue?/Expected behaviour?/Missed case? If you are using the MiniCssExtractPlugin, you are creating a separate css chunk for the related js chunk during the build. So when you are lazy loading the js module later, your js chunk is grabbed from the prefetch cache, but the related css chunk is additionally loaded through the network.

Describe the solution you'd like ideally the 1st or 2nd solution i would like: 1) Can the runtime creation of the <link rel="prefetch" href="/lazy-style.css" as="style" /> for the related lazy-loaded css chunk be handled by some available functionality of the webpack, taking into account that it does handle the same for js chunks? 2) Can we handle the static creation (during the build time) of the <link rel="prefetch" href="/lazy-style.css" as="style" /> for that separate css chunk and add it into html head with a help of html-webpack-plugin?

Describe alternatives you've considered Alternatives: 3) Can we somehow extend the functionality of the webpack to be able during the runtime add the <link rel=prefetch> for the css chunks of the related lazy loaded js chunks? 4) should we handle the static creation (during the build time) of the <link rel="prefetch" href="/lazy-style.css" as="style" /> for that separate css chunk and add it into html head on our own with a help of our custom plugin?

Additional context Partly related past questions/issues: https://github.com/jantimon/html-webpack-plugin/issues/934 https://github.com/jantimon/html-webpack-plugin/issues/1317 by @jantimon

An example - to reproduce the case when the css chunk is not prefetched. Project structure image

package.json { "name": "webpack-prefetch-test", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "start": "webpack serve --open" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.23.3", "@babel/preset-env": "^7.23.3", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "babel-loader": "^9.1.3", "css-loader": "^6.8.1", "html-webpack-plugin": "^5.5.3", "mini-css-extract-plugin": "^2.7.6", "sass": "^1.69.5", "sass-loader": "^13.3.2", "style-loader": "^3.3.3", "ts-loader": "^9.5.0", "typescript": "^5.2.2", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" } }

webpack.config.js

const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");

const templates = path.resolve(__dirname, "templates");

module.exports = {
  mode: "development",
  entry: "./src/index.tsx",
  devtool: "source-map",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
        exclude: /node_modules/,
      },
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js", "scss"],
  },
  output: {
    filename: "[name].js",
    chunkFilename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(templates, "base-template.html"),
    }),
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css",
    }),
  ],
};

tsconfig.json { "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true, "module": "ES2020", "target": "ES2020", "jsx": "react-jsx", "moduleResolution": "Bundler", "allowArbitraryExtensions": true }, "include": [ "./src/*", ] }

templates/base-template.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

src/app-component.scss h1 { color: red; font-size: 20px; }

src/app-component.tsx

import { FC, Suspense, useState } from "react";
import "./app-component.scss";

import { lazy } from "react";

const LazyComponent = lazy(
  () =>
    import(
      /* webpackPrefetch: true */
      /* webpackChunkName: "lazy-component" */
      "./lazy-component"
    )
);

const App: FC = () => {
  const [showLazy, setShowLazy] = useState(false);

  const toggleLazy = () => {
    setShowLazy(!showLazy);
  };

  return (
    <>
      <h1>This is a variable example</h1>
      <button onClick={toggleLazy}>toggle lazy component</button>
      {showLazy && (
        <Suspense fallback={<div>Loading</div>}>
          <LazyComponent />
        </Suspense>
      )}
    </>
  );
};

export default App;

src/index.tsx

import { createRoot } from "react-dom/client";
import App from "./app-component";
const root = createRoot(document.getElementById("root"));
root.render(<App />);

src/lazy-component.tsx

import "./lazy-compon.scss";

const LazyComponent = () => {
  return <div className="lazy-css">Some lazy Component</div>;
};

export default LazyComponent;

src/lazy-compon.scss .lazy-css { color: orange; }

The page loads and main.js, main.css are already in the html head (they were added during build time). The webpack during runtime adds (bcs of the wepbackPrefetch hint on js chunk) this piece of html into the head > <link rel="prefetch" as="script" href="http://localhost:8080/lazy-component.js"> . Then when clicked on a button "toggle lazy component", the css is requested from network (it is added as link stylesheet into the head), and the js is grabbed from prefetched cache.

image

image

image

alexander-akait commented 10 months ago

I don't think we can solve it here, in HTML webpack plugin, this reques is more for mini-css-extract-plugin

alexander-akait commented 6 months ago

Close in favor https://github.com/webpack-contrib/mini-css-extract-plugin/pull/1043/, release will be soon