《Webpack5 核心原理与应用实践》学习笔记-> webpack极致性能优化

简介: 《Webpack5 核心原理与应用实践》学习笔记-> webpack极致性能优化


之前一直讲的都是提升构建性能,那么如何提高生产环境下的访问性能呢?例如首页白屏,文件过大导致加载时间过长,文件太多细碎导致http通讯次数过多降低性能,http缓存失效导致资源重新拉取等问题如何解决。

动态加载



动态加载也就是懒加载,这个在VueReactSPA应用下面是很常见的优化技巧,例如Vue通过路由实现页面按需加载,那这个在webbpack中是怎么实现的呢?


由于我之前跳过了splitChunks的章节,这一部分会包含里面的内容,splitChunks我花了很多时间学习,还没完全弄明白,后面会多花一些时间单独写,下面也会把这一块的知识简单的讲一下,为后面的做铺垫。


这里就有一个概念,叫做Async Chunk,他的意思就是异步引用(使用import('./xxx.js')或者require.ensure('./xxx.js'))的模块会单独打包成一个文件,在VueRoute文件中我们通常都是使用import('./xxx.vue')来定义我们的component,如下


import { createRouter } from "vue-router";
const router = createRouter({
    route: [
        {
            path: '/',
            component: () => import('./Home.vue'), // 这里的 home 就会打包成一个单独的文件
        }
    ]
});
export default router;

其实这是webpack赋予的能力,在webpack中,一般情况下,entry定义的入口文件打包之后只会生成一个文件,如果使用到了import('./xxx.js')或者require.ensure('./xxx.js'),那么xxx.js就会单独生成一个文件,来看示例代码:


// webpack.config.js
const path = require('path');
module.exports = {
    mode: "development",
    entry: {
        main: './src/main',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: "[name].js",
    },
}
// main.js
import './common/common-sync';
// common-sync.js
console.log('common-sync')

生成的文件内容如下:

image.png


如果common-sync.js的内容过于庞大,又或者main.js直接使用import './xxx.js'同步引用的文件过多,同样也会造成最后生成的文件内容过大,这个时候如果将import './xxx.js'改成import('./xxx.js'),打包后的文件又会发生什么改变呢?来看看:


// main.js 修改
import('./common/common-sync');

构建之后的结果如下:

image.png


我们利用这一特性就可以实现按需加载,具体实现需要看业务,其实这个和懒加载图片是一个道理,在用户不使用的时候就不管他,在用户使用的时候就给用户,其实这个用Vue的路由配置来讲就很容易理解了,这里我就简单的模拟一下:


  • 配置文件先改一下,方便看效果,依赖和配置什么的之前都讲过,这里温习一下


const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode: "development",
    entry: {
        main: './src/main',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: "[name].js",
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: "./src/index.html"
        })
    ],
    devServer: {
        hot: true,
        open: true
    }
}

  • main.js内容如下


// Home要一进页面就看到,所以使用同步的方式加载,会和 main.js 打包到一起
import Home from "./Home";
Home();
document.getElementById('Home').addEventListener('click', () => {
    Home();
})
// Bar 和 Foo 使用异步的方式加载,会生成两个文件
document.getElementById('Bar').addEventListener('click', () => {
    // 这里是异步加载,只有点击对应的按钮才会请求资源
    import('./Bar').then(bar => {
        bar.default()
    });
})
document.getElementById('Foo').addEventListener('click', () => {
    import('./Foo').then(bar => {
        bar.default()
    });
})

  • index.htm页面会有对应的dom信息,如下


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <button id="Home">Home</button>
    <button id="Bar">Bar</button>
    <button id="Foo">Foo</button>
    <div id="page"></div>
</body>
</html>

  • Bar.jsFoo.js如下:


function createPage() {
    const page = document.getElementById('page');
    page.innerHTML = '<h1>Bar...</h1>'; // Foo.js就是文字不一样,就不贴代码了
}
export default createPage;

上面的配置完了之后,可以执行npx webpack查看构建出来的文件,也可以使用npx webpack server来查看对应的页面效果,查看页面效果记得打开调试模式查看网络信息。


HTTP 缓存优化


还是接着上面的例子,但是需要修改一下配置,主要是修改output.filename,完整配置如下:


const path = require('path');
module.exports = {
    mode: "development",
    entry: {
        main: './src/main',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: "[name]-[contenthash].js",
    }
}

先来说一下这个配置的意思,当然还是要参考官网output.filename,我这里就简单的复制一下课程中的解释:


  • [fullhash]:整个项目的内容 Hash 值,项目中任意模块变化都会产生新的 fullhash
  • [chunkhash]:产物对应 Chunk 的 Hash,Chunk 中任意模块变化都会产生新的 chunkhash
  • [contenthash]:产物内容 Hash 值,仅当产物内容发生变化时才会产生新的 contenthash,因此实用性较高。


然后我上面就使用[name]-[contenthash],这样只有文件内容发生改变,或者文件名发生改变,生成的资源文件名才会发生变化,现在npx webpack看一下生成的产物,然后再随便修改一下Bar.js或者Foo.js再来看一下生成的产物。


  • 第一次

image.png

  • 第二次

image.png

结果是两次生成的文件,生成的资源对应main.js的那个文件会发生改变,明明main.js里面的东西是没有改过的(如果修改Home.jsmain.js是一定会变的),这是因为Bar.js产物的引用名称发生变化了。


就是Bar.js的产物名称发生变化,对应的main.js引用的名称也要改,所以main.js的产物名称也就变了,其实main.js并没有改。


这样就导致HTTP缓存失效,因为名称改了,浏览器觉得是新的东西,就又重新加载了。


这里就又引出了一个概念叫做 运行时运行时就是webpack为了保证生成的产物能正常运行,就在生成的代码里面注入了一些代码,例如你在代码里面写了require,你在浏览器里面敲敲看,这肯定是没有的呀,这就是webpack给你提供的。


这里webpack提供了一个配置可以将该代码单独剥离出来,形成一个文件,如下:


module.exports = {
    // 省略其他配置
    optimization: {
        runtimeChunk: 'single',
    }
}

使用外置依赖


首先要弄明白什么是外置依赖,外置依赖就是项目以外的依赖文件,不归webpack管的依赖文件,就比如我有一个依赖包,他要走cdn加速,我打到代码里面去就不能使用cdn了,这个时候就可以使用外置依赖了(我可能说的不好,具体可以查看官网的解释:externals),webpack中使用externals来配置,如下:


module.exports = {
    // 省略其他配置
    optimization: {
        runtimeChunk: 'single',
    }
}

这个配置的实际作用目前我还没弄清楚,我能想到的就是我上面举的例子,或者说提升构建构建性能,利用http缓存机制,第三方库不管构建几次因为没有改动就可以一直有缓存。


使用 Tree-Shaking 删除多余模块导出


Tree-Shaking老生常谈了,中文翻译就是树摇,解释一下就是把树摇一下,把上面没用的枯树叶,或者其他没动的东西摇下来;在代码中的意思就是把用不上的模块,用不上的代码给干掉。


在 Webpack 中,启动 Tree Shaking 功能必须同时满足两个条件:


  • 配置 optimization.usedExportstrue,标记模块导入导出列表;
  • 启动代码优化功能,可以通过如下方式实现:
  • 配置 mode = production
  • 配置 optimization.minimize = true
  • 提供 optimization.minimizer 数组


简单来说就是下面这样配置就ok了:


module.exports = {
    // 省略其他配置
    mode: "production",
    optimization: {
        usedExports: true,
    }
}

还是使用上面的例子,直接在Home.js中添加多个导出语句,然后在构建一下项目看看结果吧:


function createPage() {
    const page = document.getElementById('page');
    page.innerHTML = '<h1>Home...</h1>'
}
export function func1() {
    console.log('func1')
}
export function func2() {
    console.log('func2')
}
export function func3() {
    console.log('func3')
}
export default createPage;

image.png

使用 Scope Hoisting 合并模块


合并模块是指在默认情况下,webpack会将每次引用都包装成一个函数,引用多少个模块就包装多少个,这无疑是增加了很多代码量,还是拿之前的例子来做测试,将webpack配置的mode属性值先改成development,然后修改一下main.js里面的代码如下:


import Home from "./Home";
Home();
import Bar from "./Bar";
Bar();


接着就是构建看效果,一个模块对应着一个包装函数:

image.png

这里的模块合并方法也很简单,Webpack 提供了三种开启 Scope Hoisting 的方法:


  • 使用 mode = 'production' 开启生产模式;
  • 使用 optimization.concatenateModules 配置项;
  • 直接使用 ModuleConcatenationPlugin 插件。


方法1:


const path = require('path');
module.exports = {
    mode: "production",
    entry: {
        main: './src/main',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: "[name]-[contenthash].js",
    },
}

方法2:


const path = require('path');
module.exports = {
    mode: "development",
    entry: {
        main: './src/main',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: "[name]-[contenthash].js",
    },
    optimization: {
        concatenateModules: true,
    }
}

方法3:


const path = require('path');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
    mode: "development",
    entry: {
        main: './src/main',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: "[name]-[contenthash].js",
    },
    plugins: [
        new ModuleConcatenationPlugin()
    ]
}

上面三种方式我都测试过有效,但是也是有一些限制的:


  1. 必须是 ESM 模块,原因就是其他,例如AMDCMD导入导出的内容具有动态性,无法保证会不会出问题。
  2. 模块被其他模块引用,这是为了防止重复代码,避免A模块有使用打一次,B模块也有使用再打一次。


监控产物体积


在构建的时候,控制台都会输出产物的大小,如果太大了就会抛出警告,但是并不会影响构建。

image.png

这一块的监控也是可以配置的:


module.exports = {
    // ...
    performance: {
        // 设置所有产物体积阈值
        maxAssetSize: 172 * 1024,
        // 设置 entry 产物体积阈值
        maxEntrypointSize: 244 * 1024,
        // 报错方式,支持 `error` | `warning` | false
        hints: "error",
        // 过滤需要监控的文件类型
        assetFilter: function (assetFilename) {
            return assetFilename.endsWith(".js");
        },
    },
};


上面的配置表示产物体积如果超过172KB则会报错,这里只是监控,报错并不会影响构建。


如果出现提示了,可以根据上面的方案进行优化处理。


总结


优化也要适可而止,适合自己的优化才是最好的优化,例如分包是优化,包的合并也是优化,但是这两种方案就原理是互斥的,所以要选择最合适的,过度优化可能会带来反效果。

目录
相关文章
|
1月前
|
监控 前端开发 JavaScript
Webpack 中 HMR 插件的工作原理
【10月更文挑战第23天】可以进一步深入探讨 HMR 工作原理的具体细节、不同场景下的应用案例,以及与其他相关技术的结合应用等方面的内容。通过全面、系统地了解 HMR 插件的工作原理,能够更好地利用这一功能,为项目的成功开发提供有力保障。同时,要不断关注技术的发展动态,以便及时掌握最新的 HMR 技术和最佳实践。
|
1月前
|
缓存 前端开发 JavaScript
Webpack 动态加载的原理
【10月更文挑战第23天】Webpack 动态加载通过巧妙的机制和策略,实现了模块的按需加载和高效运行,提升了应用程序的性能和用户体验。同时,它也为前端开发提供了更大的灵活性和可扩展性,适应了不断变化的业务需求和技术发展。
|
1月前
|
缓存 前端开发 JavaScript
Webpack 4 和 Webpack 5 区别?
【10月更文挑战第23天】随着时间的推移,Webpack 可能会继续发展和演进,未来的版本可能会带来更多的新特性和改进。保持对技术发展的关注和学习,将有助于我们更好地应对不断变化的前端开发环境。
|
1月前
|
缓存 前端开发 JavaScript
webpack 原理
【10月更文挑战第23天】Webpack 原理是一个复杂但又非常重要的体系。它通过模块解析、依赖管理、加载器和插件的协作,实现了对各种模块的高效打包和处理,为现代前端项目的开发和部署提供了强大的支持。同时,通过代码分割、按需加载、热模块替换等功能,提升了应用程序的性能和用户体验。随着前端技术的不断发展,Webpack 也在不断演进和完善,以适应不断变化的需求和挑战。
|
2月前
|
缓存 前端开发 JavaScript
Webpack 打包的基本原理
【10月更文挑战第5天】
|
2月前
|
缓存 前端开发 JavaScript
Webpack技术深度解析:模块打包与性能优化
【10月更文挑战第13天】Webpack技术深度解析:模块打包与性能优化
|
3月前
|
JavaScript 前端开发
手写一个简易bundler打包工具带你了解Webpack原理
该文章通过手写一个简易的打包工具bundler,帮助读者理解Webpack的工作原理,包括模块解析、依赖关系构建、转换源代码以及生成最终输出文件的整个流程。
|
4月前
|
缓存 前端开发 JavaScript
Webpack 模块解析:打包原理、构造形式、扣代码补参数和全局导出
Webpack 模块解析:打包原理、构造形式、扣代码补参数和全局导出
214 1
|
7月前
|
缓存 资源调度 监控
Webpack 5新特性详解与性能优化实践
Webpack 5通过确定性的Chunk ID、模块ID和导出ID实现了长期缓存,这意味着相同的输入将始终产生相同的输出。这样,当你的用户再次访问更新后的网站时,浏览器可以重用旧的缓存,而不是重新下载所有资源。
93 2
|
7月前
|
API 开发工具 开发者
webpack热更新原理
Webpack的Hot Module Replacement(HMR)提升开发效率,无需刷新页面即可更新模块。开启HMR需在配置中设`devServer.hot: true`。Webpack构建时插入HMR Runtime,通过WebSocket监听并处理文件变化。当模块改变,Webpack发送更新到浏览器,HMR Runtime找到对应模块进行热替换,保持应用状态。开发者可利用`module.hot` API处理热替换逻辑。