Razzle: Webpack performance plugin in CI will make build fail

Created on 19 Jun 2018  Â·  9Comments  Â·  Source: jaredpalmer/razzle

The problem

Currently, Webpack has a performance default of 244KiB (un-gzipped) where if process.env.CI = true is set, the build will fail complaining about degraded performance.

Razzle, without any additions, is already at 192KiB (un-gzipped) excluding any CSS (the above counts CSS as well).

Any reasonable additions to the base Razzle project will basically make the build fail if you are running it in a CI.

entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  client (248 KiB)
      static/css/bundle.c91c1092.css
      static/js/bundle.5bcffbdf.js

webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/

Steps to reproduce

  1. Add some tiny 2.99kB of CSS: https://gist.github.com/annez/ad5c49932358a871eb340c6773b9f4bb
  2. Add some core-js polyfills to browsers don't break - assign, find, from, promise
  3. Add some reasonable React packages - react-autosuggest for example
  4. Run CI=true npm run build and watch it fall down

Recommendations

I think it's confusing to the bundle size error on uncompressed yet we show the compressed size if the build succeeds. Also 244KiB of uncompressed JavaScript has never been an indicator of degraded performance - people should be using performance monitoring and budgeting to understand when something degrades user experience.

Several options here are:

  • Remove webpack.performance entirely from the default Razzle config
  • Set an larger upper bound limit such as 512KiB (although again, arbitrary if we are measuring real performance)
stale

Most helpful comment

I've also experienced what @mrmartineau stated.

I think it's a good thing that there is a default asset size limit; it keeps you sharp about what you include in your entry bundle.

However, I would expect the same chunks to be made _with_ and _without_ the CI environment variable set. I understand that there is a difference between a warning and an error on the CI, but I don't understand why there are more chunks when bulding locally as opposed to building in the CI.

All 9 comments

Ran into same issue. Although I think removal seems a bit of a disservice. You can use the razzle.config.js to configure this as you wish. Perhaps you would do it like this:

module.exports = {
  // ...
  modify(defaultConfig, { target, dev }, webpack) {
      if (!dev) {
        config.performance = Object.assign({}, {
          maxAssetSize: 100000,
          maxEntrypointSize: 300000,
          hints: false
        })
      }

    return config
  }
}

Hola! So here's the deal, between open source and my day job and life and what not, I have a lot to manage, so I use a GitHub bot to automate a few things here and there. This particular GitHub bot is going to mark this as stale because it has not had recent activity for a while. It will be closed if no further activity occurs in a few days. Do not take this personally--seriously--this is a completely automated action. If this is a mistake, just make a comment, DM me, send a carrier pidgeon, or a smoke signal.

ProBot automatically closed this due to inactivity. Holler if this is a mistake, and we'll re-open it.

I too have ran into this, and think it should be scrutinized in future versions. I enjoy the gesture of built in performance budgeting, but the 52KiB allowance is rather small.

I agree, but what I found problematic was that the performance.hints settings triggers a warning (or a full error if on CI) before the files have been fully compressed. In my case running razzle build on CI looks like this:

$ razzle build
Creating an optimized production build...
Compiling client...
Starting type checking and linting service...
Using 1 worker with 2048MB memory limit

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.

Failed to compile.

asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  static/js/bundle.99af500d.js (634 KiB)

entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  client (634 KiB)
      static/js/bundle.99af500d.js

error Command failed with exit code 1.

And on my local machine, it looks like this:

$ razzle build
Creating an optimized production build...
Compiling client...
Starting type checking and linting service...
Using 1 worker with 2048MB memory limit
Compiled client successfully.
Compiling server...
Compiled server successfully.
Compiled successfully.

File sizes after gzip:

  181.77 KB  build/static/js/bundle.99af500d.js
  10.04 KB   build/static/js/2.945fb472.chunk.js
  4.87 KB    build/static/js/3.c9f99c02.chunk.js
  1.9 KB     build/static/js/6.a430e3d3.chunk.js
  1.74 KB    build/static/js/4.ee533cb7.chunk.js
  1.04 KB    build/static/js/0.aec4785f.chunk.js
  574 B      build/static/js/5.11154537.chunk.js

✨  Done in 10.95s.

You can see that the final build is much smaller than the 244KiB limits set as default by webpack's performance.hints setting.

I have added @rohidee's suggestion to our razzle.config.js, but would much rather a neater way to get around this problem.

FYI, you can test how your CI might build your app but adding env CI=true before your build command, in my case it is: env CI=true yarn build

I've also experienced what @mrmartineau stated.

I think it's a good thing that there is a default asset size limit; it keeps you sharp about what you include in your entry bundle.

However, I would expect the same chunks to be made _with_ and _without_ the CI environment variable set. I understand that there is a difference between a warning and an error on the CI, but I don't understand why there are more chunks when bulding locally as opposed to building in the CI.

Can we reopen this issue by any chance? I'm still getting different bundle sizes depending on CI env variable. I tried looking into the default config here but found nothing of interest.

My workaround was to set the CI build script to CI=false yarn build so the resulting bundle is the same that I usally get when running yarn build locally, without having to disable bundle size warnings.

I got around this by:

  • Enabling the webpack split chunks plugin to make a 'vendor chunk' for all node_modules dependencies, so they are separate from my application code
  • Disabling webpack performance hints for the vendor chunk (but leaving it enabled for other ones I don't expect to be very big)

In doing this I realized that it's not very reasonable to expect razzle to calculate these hints based on gzip filesize. it's all done by webpack performance hints plugin, which operates on non-gzip.
https://webpack.js.org/configuration/performance/

My razzle.config.js:

const logger = require('razzle-dev-utils/logger')

module.exports = {
  plugins: [
    useRuntimePortEnvironmentVariable,
    WebpackSplitChunksRazzlePlugin({
      // include both initial and async imports for chunking, not just initial
      chunks: 'all',
      // default is 3
      maxInitialRequests: Infinity,
      // default is 30000
      minSize: 0,
    }),
    WebpackPerformanceHintsRazzlePlugin({
      // Use a custom assetFilter to not warn about big source maps or vendor bundle
      assetFilter: function(assetFilename) {
        const isSrcMap = /\.map$/.test(assetFilename)
        const isVendorBundle = /vendor/.test(assetFilename)
        return ! (isSrcMap || isVendorBundle)
      }
    }),
    'typescript',
  ],
}

function NoopRazzlePlugin() {
  return function NoopRazzlePluginFunc(config) {
    return config
  }
}

function WebpackPerformanceHintsRazzlePlugin(pluginOptions) {
  return function WebpackPerformanceHintsRazzlePluginFunc(config) {
    return {
      ...config,
      performance: {
        ...config.performance,
        assetFilter: function(assetFilename) {
          const isSrcMap = /\.map$/.test(assetFilename)
          const isVendorBundle = /vendor/.test(assetFilename)
          return ! (isSrcMap || isVendorBundle)
        }
      }
    }
  }
}

/**
 * Update config to use process.env.PORT provided at *runtime*, not build-time, which is the default behavior
 */
// https://github.com/jaredpalmer/razzle/issues/906#issuecomment-467046269
function useRuntimePortEnvironmentVariable(config, { target, dev }, webpack) {
  const appConfig = Object.assign({}, config);

  // @BUG: Do not inline certain env vars; https://github.com/jaredpalmer/razzle/issues/356
  if (target === 'node') {
    const idx = appConfig.plugins.findIndex(plugin => plugin.constructor.name === 'DefinePlugin');
    const { definitions } = appConfig.plugins[idx];
    const newDefs = Object.assign({}, definitions);

    delete newDefs['process.env.PORT'];
    delete newDefs['process.env.HOST'];
    delete newDefs['process.env.PUBLIC_PATH'];

    appConfig.plugins = [].concat(appConfig.plugins);
    appConfig.plugins[idx] = new webpack.DefinePlugin(newDefs)
  }

  return appConfig;
}

/**
 * Razzle Plugin to split common libraries into a chunk named 'vendor'.
 * The idea is that this bundle will change way less often than the src code
 * of this app, so will mean less average download size because vendor can be cached.
 * Taken from https://github.com/jaredpalmer/razzle/tree/master/examples/with-vendor-bundle
 */
function WebpackSplitChunksRazzlePlugin(pluginOptions={}) {
  return function WebpackSplitChunksRazzlePluginFunc (razzleConfigBefore, { target, dev }, webpack) {
    const config = Object.assign({}, razzleConfigBefore);

    // Change the name of the server output file in production
    if (target === 'web') {
      // modify filenaming to account for multiple entry files
      config.output.filename = dev
        ? 'static/js/[name].js'
        : 'static/js/[name].[hash:8].js';

      // I think these are the default that webpack sets
      // https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks
      const defaultSplitChunksConfig = {
        chunks: 'async',
        minSize: 30000,
        maxSize: 0,
        minChunks: 1,
        maxAsyncRequests: 5,
        maxInitialRequests: 3,
        automaticNameDelimiter: '~',
        automaticNameMaxLength: 30,
        name: true,
        cacheGroups: {
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
          },
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
          }
        }
      }
      config.optimization.splitChunks = {
        ...defaultSplitChunksConfig,
        ...config.optimization.splitChunks,
        ...pluginOptions,
      }
    }

    return config;
  }
}

A warning to anyone who stumbles here: for whatever reason, the solution proposed above with useRuntimePortEnvironmentVariable, WebpackSplitChunksRazzlePlugin, and WebpackPerformanceHintsRazzlePlugin will work great in development mode but can break client-side rendering in production mode.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Jayphen picture Jayphen  Â·  4Comments

JacopKane picture JacopKane  Â·  3Comments

charlie632 picture charlie632  Â·  4Comments

corydeppen picture corydeppen  Â·  3Comments

howardya picture howardya  Â·  5Comments