ChunkLoadError: Ошибка загрузки фрагмента XY. - Случайно становится фатальным на PRODUCTION

у нас уже есть наша платформа для электронной коммерции, и мы испытываем странную ошибку ChunkLoadError. Эта ошибка возникает случайным образом и не может быть воспроизведена. Когда мы пытаемся открыть неудачный файл, он есть и может быть загружен в обычном режиме.

Если пользователь получает эту ошибку, он получает белый экран (логически), но после обновления все в порядке.

Мы запускаем электронную торговлю SSR на React (последняя версия), Express (последняя версия)

наш конфиг webpack/razzle


const path = require('path');
const autoprefixer = require('autoprefixer');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const LoadablePlugin = require('@loadable/webpack-plugin');

const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserPlugin = require('terser-webpack-plugin');
// const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');

module.exports = {
  modify: (baseConfig, env, webpack) => {
    const { target, dev } = env;
    const appConfig = { ...baseConfig };
    // Setup SCSS
    if (target === 'web') {
      const filename = path.resolve(__dirname, 'build');

      const cssLoader = {
        loader: 'css-loader',
        options: {
          minimize: !dev,
          sourceMap: false,
          importLoaders: 1
        }
      };

      const postCSSLoader = {
        loader: 'postcss-loader',
        options: {
          ident: 'postcss',
          sourceMap: false,
          plugins: () => [
            autoprefixer({
              browsers: [
                '>1%',
                'last 4 versions',
                'Firefox ESR',
                'not ie < 9' // React doesn't support IE8 anyway
              ]
            })
          ]
        }
      };

      const sassLoader = {
        loader: 'sass-loader',
        options: {
          minimize: !dev,
          sourceMap: false,
          importLoaders: 1
        }
      };

      if (dev) {
        appConfig.output.filename = 'static/js/[name].js';
        appConfig.module.rules.push({
          test: /\.scss$/,
          use: ['style-loader', cssLoader, postCSSLoader, sassLoader]
        });
      } else {
        appConfig.output.filename = 'static/js/[name].[chunkhash:8].js';

        // For production, extract CSS
        appConfig.module.rules.push({
          test: /\.scss$/,
          use: [MiniCssExtractPlugin.loader, cssLoader, postCSSLoader, sassLoader]
        });

        appConfig.plugins.push(
          new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
          new webpack.IgnorePlugin(/moment/, /react-kronos/),
          new webpack.optimize.OccurrenceOrderPlugin(),
          // new webpack.optimize.LimitChunkCountPlugin({maxChunks: 50}),
          new CompressionPlugin()
          ,new BundleAnalyzerPlugin({
            analyzerMode: 'static',
            generateStatsFile: true,
            openAnalyzer: false
          })
          // ,new DuplicatePackageCheckerPlugin()
        );
      }

      // optimization
      appConfig.optimization = {
        ...baseConfig.optimization,
        minimize: !dev,
        minimizer: [new TerserPlugin({
          parallel: true,
        })],
        splitChunks: {
          chunks: 'initial',
          minSize: 30000,
          // minRemainingSize: 0,
          maxSize: 0,
          minChunks: 1,
          maxAsyncRequests: 6,
          maxInitialRequests: 4,
          automaticNameDelimiter: '~',
          automaticNameMaxLength: 30,
          cacheGroups: {
            defaultVendors: {
              test: /[\\/]node_modules[\\/]/,
              priority: -10
            },
            default: {
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true
            }
          }
        },
        moduleIds: 'total-size', //added in future deterministic
        chunkIds: 'total-size', //added
        mangleWasmImports: !dev, //added
        removeAvailableModules: !dev, //added
        mergeDuplicateChunks: !dev, //added
        flagIncludedChunks: !dev,
        occurrenceOrder: false,
        usedExports: !dev,
        // namedModules: true,
        // namedChunks: true,
        runtimeChunk: 'single'
        // runtimeChunk: {
        //   name: entrypoint => `runtimechunk~${entrypoint.name}`
        // }
      };

      appConfig.plugins.push(
        new LoadablePlugin({
          outputAsset: false,
          writeToDisk: { filename }
        }),
        new LodashModuleReplacementPlugin({
          collections: true,
          cloning: true,
          deburring: true,
          // coercions: true,
          flattening: true,
          paths: true,
          // placeholders: true
          shorthands: true
          // caching: true
        })
      );
    } else {
      appConfig.module.rules.push({
        test: /\.(scss)$/,
        use: ['ignore-loader']
      });
    }
    return appConfig;
  },
  modifyBabelOptions: mainBabelOptions => {
    return {
      ...mainBabelOptions,
      ...{ plugins: [].concat(mainBabelOptions.plugins ? mainBabelOptions.plugins : [], ['lodash']) }
    };
  }
};

несколько ошибок

Вот случайно выбранный след

(error: https://www.freshbox.sk/static/js/common-blocks-functional-userButton.3074d9ca.chunk.js)
  at m.e(webpack/bootstrap:170:18)
  at importAsync(./src/common/blocks/header/HeaderVariant2.jsx:35:9)
  at requireAsync(./src/common/blocks/header/HeaderVariant2.jsx:34:28)
  at loadAsync(./node_modules/@loadable/component/dist/loadable.esm.js:217:31)
  at componentDidMount(./node_modules/@loadable/component/dist/loadable.esm.js:147:16)
  at Ji(./node_modules/react-dom/cjs/react-dom.production.min.js:212:132)
  at b(./node_modules/react-dom/cjs/react-dom.production.min.js:255:229)
  at If(./node_modules/scheduler/cjs/scheduler.production.min.js:19:467)
  at cg(./node_modules/react-dom/cjs/react-dom.production.min.js:122:325)
  at Jj(./node_modules/react-dom/cjs/react-dom.production.min.js:248:370)
  at yj(./node_modules/react-dom/cjs/react-dom.production.min.js:239:376)
  at Ig(./node_modules/react-dom/cjs/react-dom.production.min.js:230:137)
  at bk(./node_modules/react-dom/cjs/react-dom.production.min.js:281:43)
  at a(./node_modules/react-dom/cjs/react-dom.production.min.js:284:301)
  at Nj(./node_modules/react-dom/cjs/react-dom.production.min.js:240:120)
  at ik(./node_modules/react-dom/cjs/react-dom.production.min.js:284:287)
  at hydrate(./node_modules/react-dom/cjs/react-dom.production.min.js:290:206)
  at done(./src/client/index.js:81:3)
  at checkReadyState(./node_modules/@loadable/component/dist/loadable.esm.js:428:11)
  at E/</n.push(./node_modules/@loadable/component/dist/loadable.esm.js:435:7)
  at ? (/static/js/common-components-category-listing-_default-LayoutSwitcher.869947cb.chunk.js:1:75)```

person Ivan Kopčík    schedule 15.03.2020    source источник


Ответы (1)


Если код разбивается на чанки для оптимизации загрузки, файл индекса обычно содержит имена и пути ко всем чанкам в соответствии с текущей сборкой веб-пакета. Внесение изменений в код и повторная сборка могут переименовывать фрагменты с отредактированным кодом. Браузер, однако, сначала загружает индекс вместе с определенными требуемыми фрагментами, а затем остальные фрагменты извлекаются по запросу, чтобы заставить работать всю оптимизацию разделения кода.

Я подозреваю, что в этом случае производственное развертывание происходит после загрузки индекса браузером, и некоторые фрагменты переименовываются в результате изменений при следующей сборке. Что приводит к недопустимым адресам для определенных фрагментов в устаревшем индексе. Впоследствии, когда старый индекс, который был в браузере, пытается загрузить эти несуществующие фрагменты, когда пользователь перешел к этой части веб-сайта, он выдает Loading chunk XY failed. Обновление должно обновить индекс и решить проблему.

Одним из способов решения этой проблемы было бы использование сервис-воркеров. Это может работать следующим образом:

  • Браузер загружает индекс и определенные необходимые фрагменты для отображения запрошенного раздела.
  • Когда пользователь просматривает/использует загруженный раздел, сервисный работник устанавливает в фоновом режиме и загружает все оставшиеся фрагменты в память, чтобы все фрагменты можно было обслуживать в автономном режиме по запросу. В основном создание копии текущего состояния сервера в памяти браузера. Это не только сохраняет файлы даже после обновления сервера, но и значительно сокращает время загрузки других фрагментов.
  • При новом производственном развертывании установленный сервис-воркер может изначально обслуживать локально кэшированные файлы, чтобы предотвратить сбой.
  • Сервисный работник может обновляться в фоновом режиме и обслуживать новую версию сайта, когда пользователь посещает его в следующий раз, или показывать сообщение о доступности новой версии и просить пользователя перезагрузить.

Здесь должно работать использование логики устаревания при повторной проверке или кэширования для сервис-воркеров. Надеюсь, это поможет.

person Rishit Sinha    schedule 16.03.2020
comment
спасибо, Ришит, я ценю ваш ответ. Я полностью понимаю вашу точку зрения на создание сервис-воркера, я думаю, что мы доберемся до этого. Чего я не понимаю, так это того, что, по вашим словам, это естественное поведение reactjs без сервис-воркеров? это немного касается - person Ivan Kopčík; 18.03.2020
comment
@IvanKopčík Эта проблема является побочным эффектом разделения кода, который больше похож на веб-пакет / развертывание. React не имеет к этому никакого отношения. И CRA по умолчанию создает один огромный пакет, который, хотя и не оптимален для производства, не имеет этой проблемы. Кроме того, сервис-воркеры, которые решают эту проблему с помощью разделения, очень легко реализовать, по крайней мере, с помощью CRA. - person Rishit Sinha; 18.03.2020
comment
на самом деле несколько дней назад я разговаривал об этом с несколькими парнями из DevOps, и то, что они предложили, звучало как жизнеспособный вариант. они сказали, что обычно с веб-пакетом происходит то, что index.html содержит информацию для сценариев, которые являются кусками. Итак, что можно сделать, и это специально для хостинга, сделанного в корзинах S3, так это то, что вы можете создать папку для каждой версии развертывания и в папке хранить все файлы сборки, кроме index.html. index будет на корневом уровне. таким образом, в случае нового развертывания старые файлы останутся в папке. - person Vibhanshu Biswas; 16.07.2021
comment
Это имеет смысл. Общее решение состоит в том, чтобы сделать старые фрагменты доступными для старого файла index.html. Метод, который я предложил, сделал это, сохранив эти файлы во внешнем интерфейсе, тогда как ваше предложение является тем же решением на стороне сервера. Он не ограничивается только сегментами s3, он также должен работать со многими другими настройками развертывания. Вы можете опубликовать это как отдельный ответ, так как это может быть лучшей альтернативой для многих разработчиков в зависимости от их варианта использования. - person Rishit Sinha; 17.07.2021