csstools / postcss-preset-env

Convert modern CSS into something browsers understand
https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env
Creative Commons Zero v1.0 Universal
2.22k stars 90 forks source link

Use memoized function to reduce at most 66% build time in a large Webpack project #232

Closed malash closed 2 years ago

malash commented 2 years ago

Background

Hello maintainers, recently I'm working on improving build performance of a large Webpack project, that bundled thousands of CSS files by postcss-loader and postcss-preset-env. By using Node Inspector I found there might be a performance issue in postcss-preset-env.

To prove my idea, I create a reproduce demo here.

https://github.com/malash/postcss-preset-env-issue-232

How to reproduce

  1. Setup a standard Webpack demo from https://github.com/csstools/postcss-preset-env/blob/main/INSTALL.md#webpack

  2. Create a simple .css file

/* demo.css */
.demo {
  color: red;
}
  1. Create a index.js like this.
import "./demo.css?_=0";
import "./demo.css?_=1";
import "./demo.css?_=2";
import "./demo.css?_=3";
import "./demo.css?_=4";
// ... 2000 lines

In Webpack different query like ?_=123 let Webpack treat them as different resource and run the loaders separately. This demo equals to creating 2000 CSS files and importing them in index.js.

You can just clone the reproduce demo because the steps above are already setted up.

  1. Run node --inspect-brk node_modules/.bin/webpack --progress, you will see node inspect output like this:
Debugger listening on ws://127.0.0.1:9229/77d11bef-5731-4136-bf81-181e689ce2c8
For help, see: https://nodejs.org/en/docs/inspector
  1. Open Chrome and visit chrome://inspect/#devices
  2. Click inspect link

inspect

  1. Click Start button in the Profiler tab

profiler

  1. Then, wait about 30 seconds, when the Webpack finished building, click Stop button.

  2. Change the dropdown to Heavy (Bottom Up) if needed, and click the Total Time to sort the results.

result

As you can see the getUnsupportedBrowsersByFeature cost a lot of CPU time. I believe this function can be optimized by somehow like lodash.memoize.

Temporary fix

Here is a temporary fix.

Edit node_modules/postcss-preset-env/dist/index.js:L174 and change this function

function getUnsupportedBrowsersByFeature(feature) {
  // ...
}

to this:

function _getUnsupportedBrowsersByFeature(feature) {
  // ...
}
const getUnsupportedBrowsersByFeature = require("lodash/memoize")(
  _getUnsupportedBrowsersByFeature
);

Make sure you installed lodash first.

I'm not sure is it 100% correct ( needs maintainers review), but if true, it could reduce the the build time from 30s to 10s which is 66% improvement, on my i9-10850K PC.

romainmenke commented 2 years ago

How do you add postcss-preset-env to webpack? It might make sense to make a tweak there to get the same result.

Can you share a bit of your webpack config?

malash commented 2 years ago

@romainmenke You can find the full webpack.config.js here https://github.com/malash/postcss-preset-env-issue-232/blob/master/webpack.config.js

const path = require("path");

module.exports = {
  mode: "development",
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { importLoaders: 1 } },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                config: path.join(__dirname, "postcss.config.js"),
              },
            },
          },
        ],
      },
    ],
  },
};

and postcss.config.js here https://github.com/malash/postcss-preset-env-issue-232/blob/master/postcss.config.js

/* eslint-disable global-require */
module.exports = () => ({
  plugins: [
    require("postcss-preset-env")({
      stage: 0,
    }),
  ],
});
romainmenke commented 2 years ago

Can you try this in postcss.config.js :

/* eslint-disable global-require */
const postcssPlugins = [
  require("postcss-preset-env")({
    stage: 0,
  })
];

module.exports = () => ({
  plugins: postcssPlugins,
});

And/or this in webpack.config.js :

const path = require("path");

const postcssOptions = require(path.join(__dirname, "postcss.config.js"))();

module.exports = {
  mode: "development",
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { importLoaders: 1 } },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: postcssOptions,
            },
          },
        ],
      },
    ],
  },
};

This way you share a single postcss-preset-env plugin instance which has already done the slow parts between multiple css files passing through webpack.

I did not run or test this code, so I might have made an error somewhere. But the general idea is to assign things to a variable first and then pass it to webpack.

malash commented 2 years ago

@romainmenke I tried your code but issue still exist.

But after researching, I find something interesting. If I inline the postcssOptions and disable autoloading config, the performance issue resolved.

module.exports = {
  mode: "development",
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { importLoaders: 1 } },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                config: false,
                plugins: [
                  require("postcss-preset-env")({
                    stage: 0,
                  }),
                ],
              },
            },
          },
        ],
      },
    ],
  },
};
romainmenke commented 2 years ago

Yes, that is what I was aiming for, but not familiar with postcss-loader :) Does this solve your issue fully?

Looking into performance is on our roadmap, but we want to improve test coverage first. So hoping that this helps as it might take some time before we have time for performance improvements.

malash commented 2 years ago

Temporary I use some trick method to fix the performance issue.

Looking forward for performance improvement of PostCSS and PostCSS Preset Env.