Web 性能优化: 使用 Webpack 分离数据的正确方法
WEB前端开发社区 昨天
制定向用户提供文件的最佳方式可能是一项棘手的工作。 有很多不同的场景,不同的技术,不同的术语。
在这篇文章中,我希望给你所有你需要的东西,这样你就可以:
了解哪种文件分割策略最适合你的网站和用户 知道怎么做
Bundle splitting: 创建更多更小的文件,并行加载,以获得更好的缓存效果,主要作用就是使浏览器并行下载,提高下载速度。并且运用浏览器缓存,只有代码被修改,文件名中的哈希值改变了才会去再次加载。 Code splitting:只加载用户最需要的部分,其余的代码都遵从懒加载的策略,主要的作用就是加快页面的加载速度,不加载不必要的代码。
Bundle splitting
Alice 每周访问我们的网站一次,持续 10 周 我们每周更新一次网站 我们每周都会更新我们的“产品列表”页面 我们也有一个“产品详细信息”页面,但我们目前还没有开发 在第 5 周,我们向站点添加了一个新的 npm 包 在第 8 周,我们更新了一个现有的 npm 包
基线
main.js 的文件加载。// webpack.config.jsconst path = require('path')module.exports = {entry: path.resolve(__dirame, 'src/index.js')output: {path: path.resolve(__dirname, 'dist'),filename: '[name].[contenthash].js'}}
main.js,我实际上是指 main.xMePWxHo.js,其中里面的字符串是文件内容的散列。这意味着不同的文件名 当应用程序中的代码发生更改时,从而强制浏览器下载新文件。contenthash 都会发生变化。因此,Alice 每周都要访问我们的站点并下载一个新的 400kb 文件。分解 vendor 包
main.js 和 vendor.js 文件。// webpack.config.jsconst path = require('path')module.exports = {entry: path.resolve(__dirname, 'src/index.js'),output: {path: path.resolve(__dirname, 'dist'),filename: '[name].[contenthash].js',},optimization: {splitChunks: {chunks: 'all'}}}
optimization.splitChunks.chunks ='all'的一种说法是 “将 node_modules 中的所有内容放入名为 vendors~main.js 的文件中”。main.js,但是在第一周、第8周和第5周只下载 200kb 的 vendor.js (不是按此顺序)。
分离每个 npm 包
vendor.js 遇到了与我们的 main.js 文件相同的问题——对其中一部分的更改意味着重新下载它的所有部分。react、lodash、redux、moment 等拆分成不同的文件:const path = require('path');const webpack = require('webpack');module.exports = {entry: path.resolve(__dirname, 'src/index.js'),plugins: [new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly],output: {path: path.resolve(__dirname, 'dist'),filename: '[name].[contenthash].js',},optimization: {runtimeChunk: 'single',splitChunks: {chunks: 'all',maxInitialRequests: Infinity,minSize: 0,cacheGroups: {vendor: {test: /[\\/]node_modules[\\/]/,name(module) {// get the name. E.g. node_modules/packageName/not/this/part.js// or node_modules/packageNameconst packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];// npm package names are URL-safe, but some servers don't like @ symbolsreturn `npm.${packageName.replace('@', '')}`;},},},},},};
Webpack 有一些不太聪明的默认设置,比如分割输出文件时最多3个文件,最小文件大小为30 KB(所有较小的文件将连接在一起),所以我重写了这些。 cacheGroups是我们定义 Webpack 应该如何将数据块分组到输出文件中的规则的地方。这里有一个名为 “vendor” 的模块,它将用于从node_modules加载的任何模块。通常,你只需将输出文件的名称定义为字符串。但是我将name定义为一个函数(将为每个解析的文件调用这个函数)。然后从模块的路径返回包的名称。因此,我们将为每个包获得一个文件,例如npm.react-dom.899sadfhj4.js。NPM 包名称必须是 URL 安全的才能发布,因此我们不需要 encodeURI的packageName。 但是,我遇到一个.NET服务器不能提供名称中带有@(来自一个限定范围的包)的文件,所以我在这个代码片段中替换了@。整个设置很棒,因为它是一成不变的。 无需维护 - 不需要按名称引用任何包。
main.js 文件,并且在第一次访问时仍会下载 200 KB 的npm包,但她绝不会两次下载相同的包。分离应用程序代码的区域
module.exports = {entry: {main: path.resolve(__dirname, 'src/index.js'),ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),},output: {path: path.resolve(__dirname, 'dist'),filename: '[name].[contenthash:8].js',},plugins: [new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly],optimization: {runtimeChunk: 'single',splitChunks: {chunks: 'all',maxInitialRequests: Infinity,minSize: 0,cacheGroups: {vendor: {test: /[\\/]node_modules[\\/]/,name(module) {// get the name. E.g. node_modules/packageName/not/this/part.js// or node_modules/packageNameconst packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];// npm package names are URL-safe, but some servers don't like @ symbolsreturn `npm.${packageName.replace('@', '')}`;},},},},},};
ProductList 和 ProductPage 之间共享的内容创建文件,这样我们就不会得到重复的代码。more files = 更多 Webpack 引用 more files = 不压缩

Code splitting (加载你需要的代码)
如何决定?
store、reducer、routing、actions 等都将在整个站点上共享。唯一的部分将主要是组件和它们的帮助类。7KB。 该网站的其余部分是 300 KB。 我会看着这个,然后说,我不打算把它拆分,原因如下:提前加载不会变慢。记住,你是在并行加载所有这些文件。查看是否可以记录 300KB和307KB之间的加载时间差异。
Code splitting 需要更改应用程序代码。 它引入了异步逻辑,以前只有同步逻辑。这不是火箭科学,但我认为应该通过可感知的用户体验改进来证明其复杂性。
Polyfills
// polyfills.jsrequire('whatwg-fetch');require('intl');require('url-polyfill');require('core-js/web/dom-collections');require('core-js/es6/map');require('core-js/es6/string');require('core-js/es6/array');require('core-js/es6/object');index.js 中导入这个文件。// index-always-poly.jsimport './polyfills';import React from 'react';import ReactDOM from 'react-dom';import App from './App/App';import './index.css';const render = () => {ReactDOM.render(<App />, document.getElementById('root'));}render(); // yes I am pointless, for now
import() 语法(不要与 import 语法混淆),有条件地加载polyfill 非常容易。import React from 'react';import ReactDOM from 'react-dom';import App from './App/App';import './index.css';const render = () => {ReactDOM.render(<App />, document.getElementById('root'));}if ('fetch' in window &&'Intl' in window &&'URL' in window &&'Map' in window &&'forEach' in NodeList.prototype &&'startsWith' in String.prototype &&'endsWith' in String.prototype &&'includes' in String.prototype &&'includes' in Array.prototype &&'assign' in Object &&'entries' in Object &&'keys' in Object) {render();} else {import('./polyfills').then(render);}
render() 并继续进行。import(),你需要 Babel 的动态导入插件。另外,正如 Webpack 文档解释的那样,import() 使用 promises,所以你需要将其与其他polyfill分开填充。基于路由的动态加载(特定于React)
/admin URL时,它将渲染 <AdminPage>。当Webpack 打包所有东西时,它会找到 import AdminPage from './AdminPage.js'。然后说"嘿,我需要在初始负载中包含这个"import('./AdminPage.js') ,这样 Webpack 就知道动态加载它。AdminPage,我可以创建另一个组件,当用户访问 /admin URL时将渲染该组件,它可能是这样的:// AdminPageLoader.jsimport React from 'react';class AdminPageLoader extends React.PureComponent {constructor(props) {super(props);this.state = {AdminPage: null,}}componentDidMount() {import('./AdminPage').then(module => {this.setState({ AdminPage: module.default });});}render() {const { AdminPage } = this.state;return AdminPage? <AdminPage {...this.props} />: <div>Loading...</div>;}}export default AdminPageLoader;
/admin URL),我们将动态加载 ./AdminPage.js,然后在状态中保存对该组件的引用。render 方法中,我们只是在等待 <AdminPage> 加载时渲染 <div>Loading...</div>,或者在加载并存储状态时渲染 <AdminPage>。react-loadable ,如关于 code-splitting 的React文档 中所述。总结
如果有人不止一次访问你的网站,把你的代码分成许多小文件。 如果你的站点有大部分用户不访问的部分,则动态加载该代码。
不看的原因确定内容质量低不看此公众号
赞 (0)
