jantimon/html-webpack-plugin

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

StadnykYura opened this issue · 2 comments

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 #1317 (comment), 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:
#934
#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

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

Close in favor webpack-contrib/mini-css-extract-plugin#1043, release will be soon