Производительность sass-loader в React Starter Kit

При работе над разработчиком с использованием npm run start кажется, что все в порядке с точки зрения скорости сборки веб-пакета, но в рабочей среде при запуске npm run build требуется более 5 минут для сборки приложения. Я сравнил скорость сборки со старыми версиями приложения, а также с чистой сборкой RSK и подумал, что замедление может быть вызвано sass-loader, поэтому профилировал сборку и посмотрел, что http://webpack.github.io/analyse скажет мне.

введите здесь описание изображения

Похоже, здесь действительно виноват sass-loader. Я попытался использовать fast-sass-loader, но это увеличило время сборки, а не уменьшило его. Предварительная загрузка загрузчиков, как предлагает инструмент анализа, также не помогла.

Вот файлы package.json и webpack.config.js, которые он использует.

package.json

{
  "name": "web",
  "version": "2.14.7",
  "private": true,
  "engines": {
    "node": ">='6.5'",
    "npm": ">=3.10"
  },
  "browserslist": [
    ">1%",
    "last 4 versions",
    "Firefox ESR",
    "not ie < 9"
  ],
  "dependencies": {
    "apollo-client": "^1.7.0",
    "axios": "^0.15.3",
    "babel-plugin-add-module-exports": "^0.2.1",
    "babel-polyfill": "^6.22.0",
    "bcrypt": "^1.0.2",
    "bcryptjs": "^2.4.3",
    "bluebird": "^3.5.1",
    "body-parser": "^1.16.0",
    "bootstrap": "4.0.0-beta",
    "bugsnag": "^2.0.0",
    "classnames": "^2.2.5",
    "compression": "^1.7.1",
    "connect-ensure-login": "^0.1.1",
    "cookie-parser": "^1.4.3",
    "core-js": "^2.4.1",
    "express": "^4.16.2",
    "express-graphql": "^0.6.3",
    "express-jwt": "^5.1.0",
    "express-request-language": "^1.1.9",
    "express-session": "^1.15.6",
    "fastclick": "^1.0.6",
    "fb": "^2.0.0",
    "fbjs": "^0.8.9",
    "flatpickr": "^3.0.7",
    "graphql": "^0.10.3",
    "graphql-date": "^1.0.3",
    "graphql-relay": "^0.5.1",
    "graphql-sequelize": "^5.4.2",
    "graphql-tag": "^1.2.4",
    "he": "^1.1.1",
    "history": "^4.5.1",
    "intl": "^1.2.5",
    "intl-locales-supported": "^1.0.0",
    "isomorphic-style-loader": "^4.0.0",
    "jsonwebtoken": "^7.2.1",
    "lodash": "^4.17.4",
    "mailchimp-api-v3": "^1.7.1",
    "moment": "^2.18.1",
    "node-fetch": "^1.6.3",
    "node-sass": "^4.5.3",
    "normalize.css": "^5.0.0",
    "nouislider": "^10.1.0",
    "passport": "^0.4.0",
    "passport-facebook": "^2.1.1",
    "passport-local": "^1.0.0",
    "pg": "^6.1.2",
    "pretty-error": "^2.0.2",
    "prop-types": "^15.6.0",
    "query-string": "^4.3.1",
    "react": "^16.0.0",
    "react-addons-css-transition-group": "^15.6.0",
    "react-addons-shallow-compare": "^15.6.0",
    "react-addons-transition-group": "^15.6.0",
    "react-apollo": "^1.4.3",
    "react-bootstrap": "^0.31.0",
    "react-calendar": "^1.1.0",
    "react-dates": "^12.5.1",
    "react-day-picker": "^6.1.0",
    "react-dom": "^16.0.0",
    "react-google-maps": "^8.5.0",
    "react-images": "^0.5.11",
    "react-intl": "^2.2.3",
    "react-redux": "^5.0.5",
    "react-router": "^3.0.2",
    "react-select": "^1.0.0-rc.5",
    "react-table": "^6.5.3",
    "react-toastify": "^2.0.0",
    "reactstrap": "^5.0.0-alpha.3",
    "recompose": "^0.25.0",
    "redux": "^3.7.1",
    "redux-form": "^7.0.3",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.2.0",
    "sanitize-html": "^1.15.0",
    "sass-loader": "^6.0.6",
    "sequelize": "^4.13.2",
    "sequelize-cli": "^2.5.1",
    "sequelize-slugify": "^0.5.0",
    "serialize-javascript": "^1.3.0",
    "source-map-support": "^0.4.11",
    "umzug": "^2.0.1",
    "universal-router": "^5.0.0",
    "whatwg-fetch": "^2.0.2"
  },
  "devDependencies": {
    "assets-webpack-plugin": "^3.5.1",
    "autoprefixer": "^6.7.2",
    "babel-cli": "^6.22.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^7.1.1",
    "babel-loader": "^6.2.10",
    "babel-plugin-react-intl": "^2.3.1",
    "babel-plugin-rewire": "^1.0.0",
    "babel-preset-env": "^1.1.8",
    "babel-preset-react": "^6.22.0",
    "babel-preset-react-optimize": "^1.0.1",
    "babel-preset-stage-2": "^6.22.0",
    "babel-register": "^6.22.0",
    "babel-template": "^6.22.0",
    "babel-types": "^6.22.0",
    "browser-sync": "^2.18.7",
    "chai": "^3.5.0",
    "chokidar": "^1.6.1",
    "css-loader": "^0.26.1",
    "editorconfig-tools": "^0.1.1",
    "enzyme": "^2.7.1",
    "eslint": "^3.15.0",
    "eslint-config-airbnb": "^14.1.0",
    "eslint-loader": "^1.6.1",
    "eslint-plugin-css-modules": "^2.2.0",
    "eslint-plugin-import": "^2.2.0",
    "eslint-plugin-jsx-a11y": "^4.0.0",
    "eslint-plugin-react": "^6.9.0",
    "file-loader": "^0.10.0",
    "front-matter": "^2.1.2",
    "glob": "^7.1.1",
    "json-loader": "^0.5.4",
    "lint-staged": "^3.3.0",
    "markdown-it": "^8.2.2",
    "mkdirp": "^0.5.1",
    "mocha": "^3.2.0",
    "pixrem": "^3.0.2",
    "pleeease-filters": "^3.0.0",
    "postcss": "^5.2.12",
    "postcss-calc": "^5.3.1",
    "postcss-color-function": "^3.0.0",
    "postcss-custom-media": "^5.0.1",
    "postcss-custom-properties": "^5.0.2",
    "postcss-custom-selectors": "^3.0.0",
    "postcss-flexbugs-fixes": "^2.1.0",
    "postcss-loader": "^1.2.2",
    "postcss-media-minmax": "^2.1.2",
    "postcss-nested": "^1.0.0",
    "postcss-nesting": "^2.3.1",
    "postcss-partial-import": "^3.1.0",
    "postcss-pseudoelements": "^3.0.0",
    "postcss-selector-matches": "^2.0.5",
    "postcss-selector-not": "^2.0.0",
    "postcss-url": "^5.1.2",
    "pre-commit": "^1.2.2",
    "raw-loader": "^0.5.1",
    "react-addons-test-utils": "^15.4.2",
    "react-deep-force-update": "^2.0.1",
    "react-hot-loader": "^3.0.0-beta.6",
    "redbox-react": "^1.3.3",
    "redux-mock-store": "^1.2.1",
    "rimraf": "^2.5.4",
    "sinon": "^2.0.0-pre.5",
    "stylefmt": "^5.1.1",
    "stylelint": "^7.8.0",
    "stylelint-config-standard": "^16.0.0",
    "url-loader": "^0.5.7",
    "webpack": "^2.2.1",
    "webpack-bundle-analyzer": "^2.3.0",
    "webpack-dev-middleware": "^1.10.0",
    "webpack-hot-middleware": "^2.16.1",
    "write-file-webpack-plugin": "^3.4.2"
  },
  "babel": {
    "presets": [
      [
        "env",
        {
          "targets": {
            "node": "current"
          }
        }
      ],
      "stage-2",
      "react"
    ],
    "env": {
      "test": {
        "plugins": [
          "rewire"
        ]
      }
    }
  },
  "eslintConfig": {
    "parser": "babel-eslint",
    "extends": [
      "airbnb",
      "plugin:css-modules/recommended"
    ],
    "plugins": [
      "css-modules"
    ],
    "globals": {
      "__DEV__": true
    },
    "env": {
      "browser": true
    },
    "rules": {
      "import/extensions": "off",
      "import/no-extraneous-dependencies": "off",
      "no-plusplus": [
        "error",
        {
          "allowForLoopAfterthoughts": true
        }
      ],
      "react/jsx-filename-extension": "off",
      "react/prefer-stateless-function": "off",
      "react/prop-types": "off"
    }
  },
  "stylelint": {
    "extends": "stylelint-config-standard",
    "rules": {
      "string-quotes": "single",
      "property-no-unknown": [
        true,
        {
          "ignoreProperties": [
            "composes"
          ]
        }
      ],
      "selector-pseudo-class-no-unknown": [
        true,
        {
          "ignorePseudoClasses": [
            "global"
          ]
        }
      ]
    }
  },
  "pre-commit": "lint:staged",
  "lint-staged": {
    "*.{cmd,html,json,md,sh,txt,xml,yml}": [
      "editorconfig-tools fix",
      "git add"
    ],
    "*.{js,jsx}": [
      "eslint --fix",
      "git add"
    ],
    "*.{css,less,sss}": [
      "stylefmt",
      "stylelint",
      "git add"
    ]
  },
  "scripts": {
    "lint:js": "eslint src tools",
    "lint:css": "stylelint \"src/**/*.{css,less,sss}\"",
    "lint:staged": "lint-staged || true",
    "lint": "yarn run lint:js && yarn run lint:css",
    "test": "mocha \"src/**/*.test.js\" --require babel-register --require test/setup.js",
    "test:watch": "yarn run test -- --reporter min --watch",
    "clean": "babel-node tools/run clean",
    "copy": "babel-node tools/run copy",
    "extractMessages": "babel-node tools/run extractMessages",
    "bundle": "babel-node tools/run bundle",
    "build": "babel-node tools/run build",
    "build:stats": "yarn run build -- --release --analyse",
    "deploy": "babel-node tools/run deploy",
    "render": "babel-node tools/run render",
    "serve": "babel-node tools/run runServer",
    "start": "yarn run migrate && babel-node tools/run start",
    "migrate": "sequelize db:migrate",
    "seed": "sequelize db:seed:all",
    "seed:undo": "sequelize db:seed:undo:all",
  }
}

webpack.config.js

/**
 * React Starter Kit (https://www.reactstarterkit.com/)
 *
 * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

import path from 'path';
import webpack from 'webpack';
import AssetsPlugin from 'assets-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import pkg from '../package.json';

const isDebug = !process.argv.includes('--release');
const isVerbose = process.argv.includes('--verbose');
const isAnalyse = process.argv.includes('--analyse') || process.argv.includes('--analyze');
const port = parseInt(process.env.PORT || '3000', 10);
const analyzerPort = port + 3;

// Can be `server`, `static` or `disabled`.
// In `server` mode analyzer will start HTTP server to show bundle report.
// In `static` mode single HTML file with bundle report will be generated.
// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON
// file by setting `generateStatsFile` to `true`.
let analyzerMode = 'disabled';
if (isAnalyse) {
  analyzerMode = 'server';
} else if (!isDebug) {
  analyzerMode = 'static';
}

//
// Common configuration chunk to be used for both
// client-side (client.js) and server-side (server.js) bundles
// -----------------------------------------------------------------------------

const config = {
  context: path.resolve(__dirname, '../src'),

  output: {
    path: path.resolve(__dirname, '../build/public/assets'),
    publicPath: '/assets/',
    pathinfo: isVerbose,
  },

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        include: [
          path.resolve(__dirname, '../src'),
        ],
        query: {
          // https://github.com/babel/babel-loader#options
          cacheDirectory: isDebug,

          // https://babeljs.io/docs/usage/options/
          babelrc: false,
          presets: [
            // A Babel preset that can automatically determine the Babel plugins and polyfills
            // https://github.com/babel/babel-preset-env
            ['env', {
              targets: {
                browsers: pkg.browserslist,
              },
              modules: false,
              useBuiltIns: false,
              debug: false,
            }],
            // Experimental ECMAScript proposals
            // https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-
            'stage-2',
            // JSX, Flow
            // https://github.com/babel/babel/tree/master/packages/babel-preset-react
            'react',
            // Optimize React code for the production build
            // https://github.com/thejameskyle/babel-react-optimize
            ...isDebug ? [] : ['react-optimize'],
          ],
          plugins: [
            // Adds component stack to warning messages
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
            ...isDebug ? ['transform-react-jsx-source'] : [],
            // Adds __self attribute to JSX which React will use for some warnings
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
            ...isDebug ? ['transform-react-jsx-self'] : [],
            'react-intl',
          ],
        },
      },
      {
        test: /^((?!\.local).)*\.scss$/,
        use: [
          {
            loader: 'isomorphic-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              // CSS Loader https://github.com/webpack/css-loader
              importLoaders: 1,
              sourceMap: isDebug,
              // CSS Modules https://github.com/css-modules/css-modules
              modules: true,
              localIdentName: isDebug ? '[name]-[local]-[hash:base64:5]' : '[hash:base64:5]',
              // CSS Nano http://cssnano.co/options/
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              config: './tools/postcss.config.js',
            },
          },
          {
            loader: 'sass-loader',
          },
        ],
      },
      {
        test: /\.local.scss$/,
        use: [
          {
            loader: 'isomorphic-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              // CSS Loader https://github.com/webpack/css-loader
              importLoaders: 1,
              sourceMap: isDebug,
              // CSS Modules https://github.com/css-modules/css-modules
              modules: true,
              localIdentName: '[local]',
              // CSS Nano http://cssnano.co/options/
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              config: './tools/postcss.config.js',
            },
          },
          {
            loader: 'sass-loader',
          },
        ],
      },
      {
        test: /\.css/,
        use: [
          {
            loader: 'isomorphic-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              // CSS Loader https://github.com/webpack/css-loader
              importLoaders: 1,
              sourceMap: isDebug,
              // CSS Modules https://github.com/css-modules/css-modules
              modules: true,
              localIdentName: isDebug ? '[name]-[local]-[hash:base64:5]' : '[hash:base64:5]',
              // CSS Nano http://cssnano.co/options/
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              config: './tools/postcss.config.js',
            },
          },
        ],
      },
      {
        test: /\.md$/,
        loader: path.resolve(__dirname, './lib/markdown-loader.js'),
      },
      {
        test: /\.txt$/,
        loader: 'raw-loader',
      },
      {
        test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
        loader: 'file-loader',
        query: {
          name: isDebug ? '[path][name].[ext]?[hash:8]' : '[hash:8].[ext]',
        },
      },
      {
        test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/,
        loader: 'url-loader',
        query: {
          name: isDebug ? '[path][name].[ext]?[hash:8]' : '[hash:8].[ext]',
          limit: 10000,
        },
      },
      {
        test: /\.(graphql|gql)$/,
        exclude: /node_modules/,
        loader: 'graphql-tag/loader',
      },
    ],
  },

  resolve: {
    modules: [path.resolve(__dirname, '../src'), 'node_modules'],
  },

  // Don't attempt to continue if there are any errors.
  bail: !isDebug,

  cache: isDebug,

  stats: {
    colors: true,
    reasons: isDebug,
    hash: isVerbose,
    version: isVerbose,
    timings: true,
    chunks: isVerbose,
    chunkModules: isVerbose,
    cached: isVerbose,
    cachedAssets: isVerbose,
  },
};

//
// Configuration for the client-side bundle (client.js)
// -----------------------------------------------------------------------------

const clientConfig = {
  ...config,

  name: 'client',
  target: 'web',

  entry: {
    client: ['babel-polyfill', './clientLoader.js'],
  },

  output: {
    ...config.output,
    filename: isDebug ? '[name].js' : '[name].[chunkhash:8].js',
    chunkFilename: isDebug ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js',
  },

  resolve: { ...config.resolve },

  plugins: [
    // Define free variables
    // https://webpack.github.io/docs/list-of-plugins.html#defineplugin
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
      'process.env.BROWSER': true,
      __DEV__: isDebug,
    }),

    // Emit a file with assets paths
    // https://github.com/sporto/assets-webpack-plugin#options
    new AssetsPlugin({
      path: path.resolve(__dirname, '../build'),
      filename: 'assets.json',
      prettyPrint: true,
    }),

    // Move modules that occur in multiple entry chunks to a new entry chunk (the commons chunk).
    // http://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => /node_modules/.test(module.resource),
    }),

    ...isDebug ? [] : [
      // Minimize all JavaScript output of chunks
      // https://github.com/mishoo/UglifyJS2#compressor-options
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true,
        compress: {
          screw_ie8: true, // React doesn't support IE8
          warnings: isVerbose,
          unused: true,
          dead_code: true,
        },
        mangle: {
          screw_ie8: true,
        },
        output: {
          comments: false,
          screw_ie8: true,
        },
      }),
    ],

    new BundleAnalyzerPlugin({
      // See above
      analyzerMode,
      // Host that will be used in `server` mode to start HTTP server.
      analyzerHost: '127.0.0.1',
      // Port that will be used in `server` mode to start HTTP server.
      analyzerPort,
      // Path to bundle report file that will be generated in `static` mode.
      // Relative to bundles output directory.
      reportFilename: path.resolve(__dirname, '../report.html'),
      // Automatically open report in default browser
      openAnalyzer: true,
      // If `true`, Webpack Stats JSON file will be generated in bundles output directory
      generateStatsFile: !isDebug,
      // Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.
      // Relative to bundles output directory.
      statsFilename: path.resolve(__dirname, '../stats.json'),
      // Options for `stats.toJson()` method.
      // You can exclude sources of your modules from stats file with `source: false` option.
      // See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
      statsOptions: null,
      // Log level. Can be 'info', 'warn', 'error' or 'silent'.
      logLevel: 'info',
    }),
  ],

  // Choose a developer tool to enhance debugging
  // http://webpack.github.io/docs/configuration.html#devtool
  devtool: isDebug ? 'cheap-module-source-map' : false,

  // Some libraries import Node modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  // https://webpack.github.io/docs/configuration.html#node
  // https://github.com/webpack/node-libs-browser/tree/master/mock
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
  },
};

//
// Configuration for the server-side bundle (server.js)
// -----------------------------------------------------------------------------

const serverConfig = {
  ...config,

  name: 'server',
  target: 'node',

  entry: {
    server: ['babel-polyfill', './server.js'],
  },

  output: {
    ...config.output,
    filename: '../../server.js',
    libraryTarget: 'commonjs2',
  },

  module: {
    ...config.module,

    // Override babel-preset-env configuration for Node.js
    rules: config.module.rules.map(rule => (rule.loader !== 'babel-loader' ? rule : {
      ...rule,
      query: {
        ...rule.query,
        presets: rule.query.presets.map(preset => (preset[0] !== 'env' ? preset : ['env', {
          targets: {
            node: parseFloat(pkg.engines.node.replace(/^\D+/g, '')),
          },
          modules: false,
          useBuiltIns: false,
          debug: false,
        }])),
      },
    })),
  },

  resolve: { ...config.resolve },

  externals: [
    /^\.\/assets\.json$/,
    (context, request, callback) => {
      const isExternal =
        request.match(/^[@a-z][a-z/.\-0-9]*$/i) &&
        !request.match(/\.(css|less|scss|sss)$/i);
      callback(null, Boolean(isExternal));
    },
  ],

  plugins: [
    // Define free variables
    // https://webpack.github.io/docs/list-of-plugins.html#defineplugin
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
      'process.env.BROWSER': false,
      __DEV__: isDebug,
    }),

    // Do not create separate chunks of the server bundle
    // https://webpack.github.io/docs/list-of-plugins.html#limitchunkcountplugin
    new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),

    // Adds a banner to the top of each generated chunk
    // https://webpack.github.io/docs/list-of-plugins.html#bannerplugin
    new webpack.BannerPlugin({
      banner: 'require("source-map-support").install();',
      raw: true,
      entryOnly: false,
    }),
  ],

  node: {
    console: false,
    global: false,
    process: false,
    Buffer: false,
    __filename: false,
    __dirname: false,
  },

  devtool: isDebug ? 'cheap-module-source-map' : 'source-map',
};

export default [clientConfig, serverConfig];

person RihardsPo    schedule 29.11.2017    source источник


Ответы (1)


Так как проект отлично и быстро работает в режиме разработки, разумно найти основные различия между dev и prod сборками.
Первое ценное отличие - это опция minimize в css-loader (minimize: !isDebug) и включенный плагин react-optimize. Поскольку потенциально они могут выполнять агрессивную оптимизацию, это может замедлить производственную сборку.
Во-вторых, разрешение css-подобных файлов выполняется специальным образом — с помощью функции webpack преобразователя: !request.match(/\.(css|less|scss|sss)$/i). Если целевой файл присутствует в корневом пакете три, нет необходимости выполнять такое разрешение. Если нет - возможно присутствуют лишние файлы из node_modules или еще чего.
Также можно попробовать готовое изоморфное решение с связкой frontend+backend и с поддержкой css, типа resolve-app.

person Vladislav Ihost    schedule 06.12.2017