利用长期缓存

webpack 对资源缓存有何帮助

优化应用大小之后 缩短应用加载时间使用该密钥可将应用的各个部分保留在 避免每次重新下载它们

使用软件包版本控制和缓存标头

执行缓存的常用方法是:

  1. 指示浏览器将文件缓存很长时间(例如一年):

    # Server header
    Cache-Control: max-age=31536000
    

    如果您不了解 Cache-Control 的用途,请参阅 Jake Archibald 的 关于缓存最佳的优秀博文 做法

  2. 并在该文件发生更改时对其进行重命名以强制重新下载:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

此方法指示浏览器下载 JS 文件,将其缓存并使用 缓存副本。仅当文件名改变时,浏览器才会连接到网络 (或一年过后)。

而使用 webpack 也可以执行相同的操作,但你指定的不是版本号,而是 文件哈希值。如需将哈希值包含在文件名中,请使用 [chunkhash]:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

如果您需要 文件名来将其发送到客户端,请使用 HtmlWebpackPluginWebpackManifestPlugin

HtmlWebpackPlugin 是 方法简单,但不那么灵活。在编译过程中,此插件会生成 包含所有已编译资源的 HTML 文件。如果您的服务器逻辑 那么它应该足以满足您的需求:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

通过 WebpackManifestPlugin 是一种更灵活的方法,如果您的服务器部分比较复杂,此方法非常有用。 在构建期间,它会生成一个 JSON 文件,其中包含文件名之间的映射 使用哈希值和文件名。在服务器上使用此 JSON 查找 处理哪个文件:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

深入阅读

将依赖项和运行时解压缩到单独的文件中

依赖项

应用依赖项的更改往往比实际应用代码少。如果您搬家 浏览器可以将它们分别缓存 - 而且不会在每次应用代码更改时重新下载它们。

如需将依赖项提取到单独的分块中,请执行以下三个步骤:

  1. 将输出文件名替换为 [name].[chunkname].js

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    当 webpack 构建应用时,它会替换 [name] 以区块名称命名如果不添加 [name] 部分, 通过哈希值来区分区块,这很难!

  2. entry 字段转换为对象:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    在这个代码段中,“main”是分块的名称。此名称将替换为 第 1 步中提到的 [name] 的位置。

    现在,如果您构建应用,这个代码块将包含完整的应用代码, 就像我们还没有完成这些步骤一样但这很快就会发生变化。

  3. 在 webpack 4 中,添加 optimization.splitChunks.chunks: 'all' 选项 添加到您的 webpack 配置中:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    此选项会启用智能代码拆分功能。使用它,如果遇到以下情况,Webpack 将提取供应商代码: 大小超过 30 kB(在缩减大小和进行 gzip 压缩之前)。它还将提取通用代码 - 如果您的构建生成了几个捆绑包(例如 )。

    在 webpack 3 中,添加 CommonsChunkPlugin

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    此插件接受所有模块,其中路径包含 node_modules 和 并将其移动到名为 vendor.[chunkhash].js 的单独文件中。

进行这些更改后,每次构建都将生成两个文件,而不是一个:main.[chunkhash].jsvendor.[chunkhash].js(对于 webpack 4,为 vendors~main.[chunkhash].js)。如果是 Webpack 4 如果依赖项较小,系统可能不会生成供应商软件包,这没关系:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

浏览器会分别缓存这些文件,并仅重新下载更改的代码。

Webpack 运行时代码

遗憾的是,仅提取供应商代码是不够的。如果您尝试 更改应用代码中的某些内容:

// index.js
…
…

// E.g. add this:
console.log('Wat');

您会发现 vendor 哈希值也发生了变化:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

这是因为除了模块代码之外,webpack 软件包还具有 runtime – 一小段代码 用于管理模块执行的库将代码拆分成多个文件后 这段代码首先包含区块 ID 和 相应的文件:

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack 将此运行时包含在最后生成的块中,即 vendor 每当有区块发生变化时,这段代码也会随之改变。 导致整个 vendor 分块发生变化。

为了解决此问题,我们将运行时移到单独的文件中。在 Webpack 4 中,这是指 通过启用 optimization.runtimeChunk 选项来实现:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

在 Webpack 3 中,可通过使用 CommonsChunkPlugin 创建一个额外的空块来实现此目的:

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

这些更改后,每个构建将生成三个文件:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

将它们以反向顺序添加到 index.html 中,就大功告成了:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

深入阅读

内嵌 webpack 运行时,用于保存额外的 HTTP 请求

为了进一步优化,请尝试将 webpack 运行时内嵌到 HTML 中 响应。也就是说,不要使用以下代码:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

执行此操作:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

由于运行时很小,而内嵌将有助于节省 HTTP 请求(相当不错的) 对于 HTTP/1 非常重要但对于 HTTP/2 则不那么重要,但 效果)。

以下是操作方法。

如果您使用 htmlWebpackPlugin 生成 HTML,

如果您使用 HtmlWebpackPlugin 用于生成 HTML 文件后, InlineSourcePlugin 即可:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

如果您使用自定义服务器逻辑生成 HTML

使用 webpack 4

  1. WebpackManifestPlugin 了解运行时块的生成名称:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    使用此插件的构建将创建一个如下所示的文件:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. 以方便的方式内嵌运行时块的内容。例如使用 Node.js 和 Express:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

或者,使用 webpack 3

  1. 通过指定 filename 将运行时名称设为静态:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. 以方便的方式内嵌 runtime.js 内容。例如使用 Node.js 和 Express:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

延迟加载您当前不需要的代码

有时,一个网页包含较多和不太重要的部分:

  • 如果你在 YouTube 上加载了视频页面,那么你会更关心视频,而不是 评论。在这里,视频比评论更重要。
  • 当您打开新闻网站上的某篇文章时,您会更关注 文章,而不是广告。在这里,文字比广告更重要。

在这种情况下,您可以只下载 先加载最重要的部分,稍后再延迟加载其余部分。使用 import() 函数code-splitting用于以下用途:

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

import() 指定您要动态加载特定模块。时间 webpack 检测到 import('./module.js'),则会将此模块移至单独的 chunk:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

并且仅在执行到达 import() 函数时下载。

这会缩减 main 软件包的大小,从而缩短初始加载时间。 不仅如此,它还将改善缓存,如果您更改主要分块中的代码, 评论区块不会受到影响

深入阅读

将代码拆分为路线和页面

如果您的应用有多个路线或页面,但只有一个 JS 文件 代码(单个 main 分块),那么您可能会在数据块上 。例如,当用户访问您网站的首页时:

WebFundamentals 首页

他们无需加载代码即可呈现位于其他地方的报道, 但它们会自动加载此外,如果用户总是只访问住宅 并且您对文章代码进行了更改,则 webpack 会使 而且用户必须重新下载整个应用

如果我们将应用拆分为多个页面(如果是单页应用,则为路由),用户 将仅下载相关代码此外,浏览器还会缓存应用代码 更好的办法:如果您更改主页代码, 相应的数据块

对于单页应用

如需按路由拆分单页应用,请使用 import()(请参阅“延迟加载代码” “现在不需要”部分)。如果您使用框架 它可能已有针对此问题的解决方案:

对于传统的多页应用

要按页面拆分传统应用,请使用 webpack 的条目 积分。如果您的应用有三个 网页的类型:首页、文章页和用户账号页, 应包含三个条目:

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

对于每个条目文件,webpack 将构建单独的依赖关系树并生成 一个 bundle 仅包含该条目所用的模块:

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

因此,如果只有文章页面使用 Lodash,则 homeprofile 捆绑包 不会包含该库,并且用户无需下载该库, 访问首页。

不过,单独的依存关系树有其缺点。如果两个入口点使用 而且你还没有将依赖项移到供应商软件包中 积分将包含 Lodash 副本。要解决此问题,请在 webpack 4 中添加 optimization.splitChunks.chunks: 'all' 选项添加到您的 webpack 配置中:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

此选项会启用智能代码拆分功能。如果选中这个选项 查找通用代码并将其提取到单独的文件中。

或者,在 Webpack 3 中,使用 CommonsChunkPlugin - 它会将常用依赖项移到新的指定文件中:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

您可以随意使用 minChunks 值来找到最佳值。一般来说, 您希望将其保持在较小的范围内,但在数据块数量增加时增加该值。对于 例如,如果是 3 个分块,minChunks 可能是 2,但对于 30 个分块,可能是 8 因为如果将其保持为 2,就会有太多模块进入通用文件, 不要过度膨胀它。

深入阅读

使模块 ID 更稳定

构建代码时,webpack 会为每个模块分配一个 ID。之后,系统会将这些 ID bundle 内的 require() 中使用。您通常会在 build 输出中看到 ID 就在模块路径前面:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ 此处

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

默认情况下,使用计数器计算 ID(即第一个模块的 ID 为 0, 第二个广告的 ID 为 1,依此类推)。这种方法的问题在于,如果将 新模块就会显示在模块列表中间 后续单元ID:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ 我们新增了 模块...

[4] ./webPlayer.js 24 kB {1} [built]

↓ 看看它的成效!comments.js 现在的 ID 为 5,而不是 4

[5] ./comments.js 58 kB {0} [built]

ads.js 现在的 ID 为 6,而非 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

这会使所有包含或依赖于 ID 已更改的模块的区块失效: 即使其实际代码没有更改。在本例中,0 分块(即数据块 使用 comments.js)和 main 分块(具有其他应用代码的分块) 已失效,而只有 main 应该已失效。

要解决此问题,请使用 HashedModuleIdsPlugin。 它将基于计数器的 ID 替换为模块路径的哈希值:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ 此处

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

使用此方法,只有重命名或移动模块时,模块的 ID 才会改变 模块。新模块不会影响其他模块的。

如需启用该插件,请将其添加到配置的 plugins 部分:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

深入阅读

汇总

  • 通过更改软件包名称来缓存软件包并区分各个版本
  • 将 bundle 拆分为应用代码、供应商代码和运行时
  • 内嵌运行时以保存 HTTP 请求
  • 使用 import 延迟加载非关键代码
  • 按路线/页面拆分代码,以避免加载不必要的内容