前端性能优化可以分为两大类分别是页面级别优化包含了http请求数以及内联脚本位置优化,代码级别的优化包含DOM操作优化,CSS选择符优化以及图片优化等
前端里面包含的内容是丰富的,它包括HTML,CSS以及JS和图片等各种各样的资源。因此前端优化是复杂的和必要的
1. 减少 HTTP 请求
- Queueing: 在请求队列中的时间。
- Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
- Proxy negotiation: 与代理服务器连接进行协商所花费的时间。
- DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
- Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
- SSL: 完成SSL握手所花费的时间。
- Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。
- Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间总和,它包含了 DNS 解析时间、 TCP 连接时间、发送 HTTP 请求时间和获得响应消息第一个字节的时间。
- Content Download: 接收响应数据所花费的时间。
从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。
http协议是无状态的应用层协议,意味着每次http请求都需要建立通信链路、进行数据传输,而在服务器端,每个http都需要启动独立的线程去处理。这些通信和服务的开销都很昂贵,减少http请求的数目可有效提高访问性能。 减少http的主要手段是合并CSS、合并javascript、合并图片。将浏览器一次访问需要的javascript和CSS合并成一个文件,这样浏览器就只需要一次请求。图片也可以合并,多张图片合并成一张,如果每张图片都有不同的超链接,可通过CSS偏移响应鼠标点击操作,构造不同的URL。 缓存的力量是强大的,恰当的缓存设置可以大大的减少 HTTP请求。假设某网站首页,当浏览器没有缓存的时候访问一共会发出 78个请求,共 600多 K数据,而当第二次访问即浏览器已缓存之后访问则仅有 10个请求,共 20多 K数据。 (这里需要说明的是,如果直接 F5刷新页面的话效果是不一样的,这种情况下请求数还是一样,不过被缓存资源的请求服务器是 304响应,只有 Header没有Body ,可以节省带宽 )
2.静态资源使用 CDN
内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
3.将 CSS 放在文件头部,JavaScript 文件放在底部
所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。
那为什么 CSS 文件还要放在头部呢?
因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。
另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。
4.压缩文件
压缩文件可以减少文件下载时间,让用户体验性更好。
得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。
5.图片优化
- 雪碧图;
- 压缩图片(tinypng.com/);
- 不影响页面质感的前提下降低图片质量;
- 小图标使用阿里矢量图的iconfont
- 预加载与懒加载
6.减少重绘重排
重排
当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。
重绘
当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排 。
重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。
什么操作会导致重排?
- 添加或删除可见的 DOM 元素
- 元素位置改变
- 元素尺寸改变
- 内容改变
- 浏览器窗口尺寸改变
如何减少重排重绘?
- 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
- 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。
7.优化首屏加载时间
-
cdn分发(减少传输距离)。通过在多台服务器部署相同的副本,当用户访问时,服务器根据用户跟哪台服务器距离近,来决定哪台服务器去响应这个请求。
-
后端在业务层的缓存。数据库查询缓存是可以设置缓存的,这个对于处于高频率的请求很有用。浏览器一般不会对
content-type: application/json的接口进行缓存,所以有时需要我们手动地为接口设置缓存。比如一个用户的签到状态,它的缓存时间可以设置到明天之前。 -
静态文件缓存方案。这个最常看到。现在流行的方式是文件hash+强缓存的一个方案。比如hash+ cache control: max-age=1年。
-
前端的资源动态加载:
a. 路由动态加载,最常用的做法,以页面为单位,进行动态加载。 b. 组件动态加载(offScreen Component),对于不在当前视窗的组件,先不加载。 c. 图片懒加载(offScreen Image),同上。值得庆幸的是,越来越多的浏览器支持原生的懒 加载,通过给img标签加上loading="lazy来开启懒加载模式。 -
合并请求。这点在http1.1比较明显,因为http1.1的请求是[串行
-
页面使用骨架屏。意思是在首屏加载完成之前,通过渲染一些简单元素进行占位。骨架屏的好处在于可以减少用户等待时的急躁情绪。这点很有效,在很多成熟的网站都有大量应用。没有骨架屏的话,一个loading图也是可以的。
-
使用ssr服务端渲染。(www.jianshu.com/p/10b6074d7…
-
引入http2.0。http2.0对比http1.1,最主要的提升是传输性能,在接口小而多的时候会更加明显。
-
利用好http压缩。即使是最普通的gzip,也能把
bootstrap.min.css压缩到原来的17%。可见,压缩的效果非常明显,特别是对于文本类的静态资源。另外,接口也是能压缩的。接口不大的话不用压缩,因为性价比低(考虑压缩和解压的时间)。 -
利用好script标签的async和defer这两个属性。功能独立且不要求马上执行的js文件,可以加入async属性。如果是优先级低且没有依赖的js,可以加入defer属性。
-
(少用)选择先进的图片格式。使用 WebP 的图片格式来代替现有的jpeg和png,当页面图片较多时,这点作用非常明显。把部分大容量的图片从BaseLine JPEG切换成Progressive JPEG也能缩小体积。
8.webpack打包优化
1.禁用webpack的devtools
-
打包出来的js文件非常大,每个js文件竟然达到了3~4Mbs
-
究其原因,是因为webpack里面启用了sourceMap,以便于调试。但是这在发布以后就完全没有用了。
-
webpack配置,里面有这句话,把这句话注释掉。原本3~4Mbs的文件,变成了1Mbs文件。压缩了3倍以上。
// 启用sourceMap devtool: '#source-map'
2. 抽离css样式等
// *************webpack需要引入的包*************************
// 抽离css样式
let MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 用来压缩分离出来的css样式
let OptimizeCss = require('optimize-css-assets-webpack-plugin');
// 用来压缩js
let UglifyJsPlugin = require('uglifyjs-webpack-plugin');
// *************webpack相关配置部分*************************
module.exports = {
optimization: {
// 优化项
minimizer: [
new OptimizeCss(), // 压缩css
new UglifyJsPlugin({ // 压缩js
cache: true, // 是否用缓存
parallel: true, // 并发打包
sourceMap: false, // es6 -> es5 转换时会用到
}),
],
}
// 中间部分省略
// 抽离css样式
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].css', // 抽离出来样式的名字
}),
],
}
3.启用依赖关系可视化工具(webpack-bundle-analyzer)
// 依赖关系可视化
// *************webpack需要引入的包*************************
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
// *************webpack相关配置部分*************************
module.exports = {
// 启动依赖关系可视化窗口,绑定端口8919
plugins: [
new BundleAnalyzerPlugin({ analyzerPort: 8919 }),
],
}
根据出现的可视化图可以看到哪些文件比较大,从而进行细化优化
分析包大小问题:
- ant-design的Icons文件
- moment文件
- quill文件
- highlight.js文件
3.1 ant-design的Icons文件优化
-
按需引入需要的icon文件
// 自己项目里面用到的Icon export { default as FileOutline, } from '@ant-design/icons/lib/outline/FileOutline';
// antd的message组建内部用到的Icon 把源代码复制过来。 // var iconType = { // info: 'info-circle', // success: 'check-circle', // error: 'close-circle', // warning: 'exclamation-circle', // loading: 'loading' // }[args.type]
// message info export { default as InfoCircleTwoTone, } from '@ant-design/icons/lib/twotone/InfoCircleTwoTone';
// message success export { default as CheckCircleTwoTone, } from '@ant-design/icons/lib/twotone/CheckCircleTwoTone';
// message error export { default as CloseCircleTwoTone, } from '@ant-design/icons/lib/twotone/CloseCircleTwoTone';
// message warning export { default as ExclamationCircleTwoTone, } from '@ant-design/icons/lib/twotone/ExclamationCircleTwoTone';
// message loading export { default as LoadingOutline, } from '@ant-design/icons/lib/outline/LoadingOutline';
-
修改 webpack 配置
module.exports = { resolve: { modules: [path.resolve(__dirname, './src'), 'node_modules'], // <- 追加代码 extensions: ['.ts', '.js', '.vue', '.json'], // <- 追加代码 alias: { vue': path.resolve(__dirname, './src/tools/antdIcon.ts'), // <- 追加代码 }, plugins: [ // <- 追加代码 new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, './tsconfig.json'), }), ], },
3.2 moment文件优化
-
这是个Ant-design内部依赖的语言文件,我的程序里面本身没有引用,我主要用到的是里面的中文,所以,中文以外的我全部在webpack里面设置忽略就行了
module.exports = { plugins: [ // 只读取(zh-cn)文件。 new webpack.ContextReplacementPlugin(/moment[\/]locale/, /^\.\/(zh-cn)/), ], }
3.3 quill文件优化
-
这个是我用到的富文本功能,本身对这个组件不太了解,但又必须要用到,也没什么太好优化方法,索性,把它抽离成一个单独的共通JS文件,这样起码有2个组件同时调用这个富文本的情况下,只有第一个掉用的那个需要引入JS文件,第二次的那个会直接利用浏览器的缓存来调用这个JS文件的,也有一定程度的优化效果。
module.exports = { optimization: { splitChunks: { cacheGroups: { vendor: { chunks: 'all', test: /\/[\/]/, // <- 就是简单修改了下匹配规则 name: 'vendor', minChunks: 1, maxInitialRequests: 5, minSize: 0, priority: 100, }, common: { chunks: 'all', test: /[\/]src[\/]js[\/]/, name: 'common', minChunks: 2, maxInitialRequests: 5, minSize: 0, priority: 1, }, }, }, runtimeChunk: { name: 'manifest', }, } }
3.4 highlight.js文件优化
-
这个主要是我用markdown编辑器的时候,用来给文字着色的。没有这个,在编写markdown的时候,内容非常的丑陋。
-
但是这个东西的语法太多了,导致这个包非常的大,我编写的时候,只需要利用其中的几种情况而已,我先随便定几种情况,反正是自己的项目,有不够的以后随时再追加
// 按需加载的写法 import hljs from 'highlight.js/lib/highlight'; import javascript from 'highlight.js/lib/languages/javascript'; hljs.registerLanguage('javascript', javascript);
4.抽离共通部分
为了方便我调查,我把共通的依赖部分都抽离出来了。而这个功能是webpack4自带的,如果是之前或者更早版本的webpack,需要引入第三方组件CommonsChunkPlugin这里不做介绍。
module.exports = {
optimization: {
//打包 公共文件
splitChunks: {
cacheGroups: {
vendor: {
//node_modules内的依赖库
chunks: 'all',
test: /[\\/](node_modules)[\\/]/, // 文件路径里面带有node_modules 都抽离出来做共通
name: 'vendor',
minChunks: 1, //被不同entry引用次数(import),1次的话没必要提取
maxInitialRequests: 5,
minSize: 0,
priority: 100,
// enforce: true?
},
common: {
// ‘src/js’ 下的js文件
chunks: 'all',
test: /[\\/]src[\\/]js[\\/]/, //也可以值文件/[\\/]src[\\/]js[\\/].*\.js/,
name: 'common', //生成文件名,依据output规则
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: 1,
},
},
},
runtimeChunk: {
name: 'manifest',
},
}
}
5.配置 resolve.modules(大约缩短十几秒)
(1)webpack 的 resolve.modules 是用来配置模块库(即 node_modules)所在的位置。当 js 里出现 import 'vue' 这样不是相对、也不是绝对路径的写法时,它便会到 node_modules 目录下去找。
(2)在默认配置下,webpack 会采用向上递归搜索的方式去寻找。但通常项目目录里只有一个 node_modules,且是在项目根目录。为了减少搜索范围,可我们以直接写明 node_modules 的全路径。
打开 build/webpack.base.conf.js 文件:
module.exports = { resolve: { extensions: ['.js', '.vue', '.json'], modules: [ resolve('src'), resolve('node_modules') ], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), } },
6.配置装载机的 include & exclude
(1)webpack 的装载机(loaders)里的每个子项都可以有 include 和 exclude 属性:
- include:导入的文件将由加载程序转换的路径或文件数组(把要处理的目录包括进来)
- exclude:不能满足的条件(排除不处理的目录)
(2)我们可以使用 include 更精确地指定要处理的目录,这可以减少不必要的遍历,从而减少性能损失。
(3)同时使用 exclude 对于已经明确知道的,不需要处理的目录,予以排除,从而进一步提升性能。
打开 build/webpack.base.conf.js 文件,第 7 行的 easytable 如果项目没用到可以把它从 include 中去除。
module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig, include: [resolve('src'), resolve('node_modules/vue-easytable/libs')], exclude: /node_modules\/(?!(autotrack|dom-utils))|vendor\.dll\.js/ }, { test: /\.js$/, loader: 'babel-loader', include: [resolve('src')], exclude: /node_modules/ },
7.使用 webpack-parallel-uglify-plugin 插件来压缩代码(可以缩短50多秒)
1)默认情况下 webpack 使用 UglifyJS 插件进行代码压缩,但由于其采用单线程压缩,速度很慢。
(2)我们可以改用 webpack-parallel-uglify-plugin 插件,它可以并行运行 UglifyJS 插件,从而更加充分、合理的使用 CPU 资源,从而大大减少构建时间。
打开 build/webpack.prod.conf.js 文件,并作如下修改:
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');//.... // 删掉webpack提供的UglifyJS插件 //new UglifyJsPlugin({ // uglifyOptions: { // compress: { // warnings: false // } // }, // sourceMap: config.build.productionSourceMap, // parallel: true //}), // 增加 webpack-parallel-uglify-plugin来替换 new ParallelUglifyPlugin({ cacheDir: '.cache/', uglifyJS:{ output: { comments: false }, compress: { warnings: false } } }),
8.使用 HappyPack 来加速代码构建
(1)由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情只能一件一件地做,不能多件事一起做。
(2)而 HappyPack 的处理思路是:将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展多进程模式,从而加速代码构建。
打开 build/webpack.base.conf.js 文件,并作如下修改:
const HappyPack = require('happypack');const os = require('os');const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); module.exports = { module: { rules: [ { test: /\.js$/, //把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行 loader: 'happypack/loader?id=happyBabel', include: [resolve('src')], //排除node_modules 目录下的文件 exclude: /node_modules/ }, ] }, plugins: [ new HappyPack({ //用id来标识 happypack处理那里类文件 id: 'happyBabel', //如何处理 用法和loader 的配置一样 loaders: [{ loader: 'babel-loader?cacheDirectory=true', }], //共享进程池 threadPool: happyThreadPool, //允许 HappyPack 输出日志 verbose: true, }) ]}
9.利用 DllPlugin 和 DllReferencePlugin 预编译资源模块
(1)我们的项目依赖中通常会引用大量的 npm 包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,而下面介绍的两个插件就是用来规避此类损耗的:
- DllPlugin 插件:作用是预先编译一些模块。
- DllReferencePlugin 插件:它的所用则是把这些预先编译好的模块引用起来。
(2)注意:DllPlugin 必须要在 DllReferencePlugin 执行前先执行一次,dll 这个概念应该也是借鉴了 windows 程序开发中的 dll 文件的设计理念。
在 build 文件夹中新建 webpack.dll.conf.js 文件,内容如下(主要是配置下需要提前编译打包的库):
const path = require('path');const webpack = require('webpack'); module.exports = { entry: { vendor: ['vue/dist/vue.common.js', 'vue-router', 'axios', 'mint-ui', 'vue-cordova', '@fortawesome/fontawesome-svg-core', '@fortawesome/free-solid-svg-icons', '@fortawesome/free-regular-svg-icons', '@fortawesome/free-brands-svg-icons', '@fortawesome/vue-fontawesome'] }, output: { path: path.join(__dirname, '../static/js'), filename: '[name].dll.js', library: '[name]_library' // vendor.dll.js中暴露出的全局变量名 }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, '.', '[name]-manifest.json'), name: '[name]_library' }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ]};
编辑 package.json 文件,添加一条编译命令:
{ "name": "ddjk_vue", "version": "1.0.0", "description": "A Vue.js project", "author": "Boss", "private": true, "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "build": "node build/build.js", "build:dll": "webpack --config build/webpack.dll.conf.js" },
执行 npm run build:dll 命令来生成 vendor.dll.js。
注意:如果之后这些需要预编译的库又有变动,则需再次执行 npm run build:dll 命令来重新生成 vendor.dll.js
index.html 这边将 vendor.dll.js 引入进来。
打开 build/webpack.base.conf.js 文件,编辑添加如下高亮配置,作用是通过 DLLReferencePlugin 来使用 DllPlugin 生成的 DLL Bundle。
const webpack = require('webpack'); module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, //..... plugins: [ // 添加DllReferencePlugin插件 new webpack.DllReferencePlugin({ context: path.resolve(__dirname, '..'), manifest: require('./vendor-manifest.json') }), ]}