theme: smartblue
在前端发展日新月异的今天,我们前端能做的事情越来越多,但随之而来的是我们的项目越来越大,功能越来越复杂,这个时候前端性能优化避免不了的话题。大家可能或多或少都有了解/学习过一些优化的知识,但是你是否系统去学过呢?或者你在有需求的时候是否可以去解决呢?今天这篇文章,我将带大家一起学习一些前端常见的性能优化手段以及对应的知识原理!!
本文适合:
- 对性能优化感兴趣的同学
- 有面试需求的同学
本文将学到:
-构建工具的优化
-图片的优化
-利用缓存做优化
-利用CDN做优化
话不多说,就让我们开始今天的学习吧!!!
1.构建工具方面的优化
现在市面上有很多成熟好用的构建工具,但是使用率最高的还得是功能强大齐全的webpack 以及这两年异军突起的vite,接下来我们就一起看看如何用这两个工具进行性能优化
1.1 webpack
接下来我们一起看看webpack在性能优化相关的知识吧
- 使用高版本的 webpack 和node
- 多进程构建:使用thread-loader(HappyPack不维护了,这里不推荐)
- 使用Tree shaking 删除多余模块导出
- 配置
optimization.usedExports
为true
启动标记功能 - 启动代码优化功能 可以通过如下方法实现
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
- 配置
- 使用Scope Hoisting合并模块
- Scope Hoisting用于 将符合条件的多个模块合并到同一个函数空间 中,从而减少产物体积,优化性能。
- 开启方法:
mode = 'production'
开启生产模式- 使用
optimization.concatenateModules
配置项 - 使用
ModuleConcatenationPlugin
插件
- 开启模块热替换
- 可以通过
devServer:{hot:true}
或者 使用HotModuleReplacementPlugin
开启模块热替换 - 忽略部分很少变化的大文件如node_modules提高构建效率
- 可以通过
- 监控产物体积
- 监控产物体积可以帮助我们分析项目的性能,避免项目体积过大带来的资源消耗
- 通过 performance 配置项来自定义各种阈值或参数
业内认为一般情况下应该保证关键路径的资源体积始终小于 170kb,如果超过这个大小,可能就需要考虑优化来减小体积
缩小文件的搜索范围
优化loader配置:可以通过
test/ include / exclude
来指定文件的loader命中的文件范围,可以通过指定include
来使 loader只处理那些需要被处理的模块优化
resolves.modules
配置:用于指定webpack去哪些路径下寻找第三方模块- 例如当所有第三方模块都放在 node_modules时 可以配置
resolve: {modules: path.resolve(__dirname, 'node_modules')}
- 例如当所有第三方模块都放在 node_modules时 可以配置
优化resolve.mainFilelds配置:用于配置第三方模块使用哪个入口文件 -为了减少搜索范围,可以使用
resolve: {mainFields: ['main']}
-如果想优先使用ESModule版本的话,设置
resolve: {mainFields: ['jsnext:main', 'main']}
配置
resolve.alias
:resolve.alias
通过别名将原导入路径映射成一个新的导入路径配置
resolve.extensions
: 引入文件时省略数组内的后缀名配置
resolve.noParse
: 省略对指定文件的处理,如(JQuery等大型库)可以提高构建性能(被忽略的文件不能包含导入语句如require / import / define
)
设置环境
- 设置
mode: production/development
可以开启对应的优化
- 设置
代码压缩
- 使用
terser-webpack-plugin
压缩ES6代码 - 使用
ParalleUglifyPlugin
多进程压缩代码 - 使用
css-minimize-webpack-plugin
对css代码进行压缩 - 使用
html-minimizer-webpack-plugin
压缩html代码
- 使用
使用CDN加速
将静态资源存储在
CDN
上可以加快对静态资源的访问速度,减少流量消耗
- 通过
output.publicPath
设置JavaScript文件地址 - 通过
WebPlugin.stylePublicPath
设置CSS文件的地址 - 通过
css-loader.publicPath
设置被CSS导入的资源的地址
- 通过
为不同的环境配置对应的配置文件
- 使用
webpack-merge
分别书写development/ production / test环境下的配置文件
- 使用
使用缓存构建
- 配置
cache: {type: 'systemfile'}
开启构建缓存,可以大幅提高二次构建的速度
- 配置
使用DllPlugin:使用DllPlugin进行分包,使用
DllReferencePlugin
引用mainfext.json
, 通过将一些很少变动的代码先打包成静态资源,避免重复编译来提高构建性能提取公共代码
- 使用
splitChunkPlugin
提取公共代码,减少代码体积 (webpack3通过CommonsChunkPlugin
)
- 使用
动态Polyfill:使用
polyfill-service
只返回给用户需要的polyfill使用可视化工具来分析性能
- 使用UnusedWebpackPlugin分析未被使用到的文件
- 使用Webpack Dashboard 以命令行的形式输出编译过程的各种信息
- 使用Webpack Bundle Analyzer分析重复的模块或者没被用到的模块
- 使用
--json=stats,json
将构建过程中的信息都输出到指定文件 - 使用Webpack Analysis:官方提供的可视化分析工具
以上就是使用webpack进行优化的常见手段了,对于webpack 感兴趣的同学可以看看这篇关于webpack的好文:一文带你读懂webpack的知识和原理,附带常见面试题! - 掘金 (juejin.cn)
1.2 vite
讲完webpack,接下来我们看看如何使用vite来做性能优化。关于vite的优化可以从以下几个方面入手。
1.网络优化
1.1 HTTP2
http1的缺点:
- 队头阻塞:当前列的队头数据包受阻而导致整列数据包阻塞的现象
- 请求排队:同一个TCP管道同一个时刻只能处理一个HTTP请求
- 并发请求数量有限:同一个域名下的并发请求数量有限制
http2的改进:
- 多路复用:通过将数据分成多个二进制帧,使得多个请求和响应数据帧可以在同一个TCP通道进行传输 解决了队头阻塞的问题
- 首部压缩:
http2
通过在客户端和服务端使用 “首部表”来跟踪和存储之前发送的请求头的键值对,对于相同的数据将不再每次请求都重复发送,达到节省流量和缩短请求时间的效果 - 设置优先级:可以对请求设置优先级 来对一些比较紧急的请求优先处理
- Server Push: 通过服务端推送可以让某些资源提前到达浏览器
在 vite
中可以通过 vite-plugin-mkcert
在 dev server
开启 HTTP2
1.2 DNS预解析
可以通过 dns-prefetch
将DNS
解析的过程提前,降低 DNS
解析的延迟时间
1.3 preload
1 | <link rel="preload" href="important.js" as="script"> ;//href表示资源地址 as表示资源类型 |
以上就是 preload
的使用方式 即对于一些比较重要的资源 可以用过预加载的方式来使资源更早地到达浏览器,优化性能
在 vite
中可以build.polyfillModulePrelad
一键开启 modulepreload
的 Polyfill 来让所有支持原生 ESM 的浏览器都可以兼容此特性
1 | //vite.congfig.ts |
1.4 prefetch
在浏览器空闲的时候去预加载其他页面的资源 (浏览器兼容性不太好 所以用的不是很多)
1.5 预构建
将第三方依赖内部的文件合并为一个文件,减少HTTP请求数量
2.资源优化
2.1 资源压缩
分为对 javascript代码
、 css代码
、图片等其他资源
三大类
其中 vite
默认对 JavaScript代码
和 css代码
都有开启压缩,一般不用我们自己去配置。而对于图片资源, 可以使用 vite-plugin-imagemin
来压缩。
2.2 产物拆包
为什么要进行产物拆包呢?如果不对产物拆包,会有下面几个问题
- 首屏性能下降:首屏加载所有代码,造成体积过大,首屏卡顿
- 线上缓存效果差:只要有代码改动就会导致整个产物缓存失效,缓存复用率几乎为0
那我们看看vite
对于产物的拆包策略是啥
- CSS代码分割:实现一个
chunk
对应一个CSS文件 - 将业务代码和第三方依赖的代码分开打包
- 对于动态
import
的模块单独打包成一个chunk
可以通过 manualChunks
配置项自定义拆包策略(一般不用自己配置)
2.3 构建产物分析
分析构建产物可以帮助我们对项目的性能做优化,这里我推荐使用 rollup-plugin-visualizer
进行产物分析
使用方式也很简单
1 | //vite.config.ts |
2.4 按需加载
vite
会将动态 import
的模块单独打包成一个chunk
而不同的框架一般也会有自己的按需加载方案 这个根据时机情况自己选择就好
2.5 预渲染优化
预渲染优化可分为 服务端渲染(SSR)和 静态站点生成(SSG)
- SSR: 服务端生成完整的 HTML内容,浏览器接收到后可以直接渲染出内容而不需要经过JS的加载,降低了浏览器的压力,同时有利于SEO
- SSG:在构建阶段生成完整的HTML内容 (SSR是在服务器运行时生成) 适合做较为静态的站点(比如博客)
关于vite的常用性能优化手段就讲完了,接下来我们来学习以下如何优化资源占比大头的图片资源
2.图片优化
图片资源一直是各种资源占比很大的一种,所以我们想要优化前端性能,就不可避免地要在图片资源上面下功夫!
常见的图片资源类型
-JPG/JPEG: 可以通过有损压缩减小体积,常用于banner、轮播图、背景图
等色彩丰富但对对比度要求低的场景
-PNG:无损压缩但体积较大,常用于颜色简单对比度高的场景,如logo图
-svg:文本文件,体积小,压缩效率高,常用作矢量图或logo(掘金的logo就是svg格式)
-base64:文本文件,为小图标而生(webpack的url-loader
可以根据文件大小判断是否采用base64格式转换)
-gif:无损压缩的动画格式
看完图片的类型及对应的特点,你应该知道如何根据使用场景来选择不同格式的图片了,接下来我们讲讲如何从代码层面来通过图片优化性能
-懒加载:如果一次性加载所有图片的话,会造成很大的性能浪费和卡顿,所以最好对图片进行懒加载,这可以极大地提高性能和使用体验(如何懒加载网上有很多资源,可以自行查阅)
-响应式图片:即根据屏幕大小自动加载合适的图片,可以通过picture
和 @media
实现
-使用CSS代替图片:对于一些背景(比如渐变)可以使用css来实现而不是图片
-压缩图片体积:比如webpack的image-webpack-loader
和vite 的 vite-plugin-imagemin
都是压缩图片的利器
关于图片方面的优化大概就是上面这么多了,接下来我们趁热打铁来学习下性能优化的重中之重-缓存
3.本地缓存
我们前端说的本地缓存一般指的是浏览器的本地缓存(webStorage
),而本地缓存是我们做性能优化时的一大关注点,接下来我就带大家一起学习下如何利用本地缓存来做性能优化!
缓存分类:
1.Memory Cache:再浏览器内存中的缓存,访问优先级最高,访问速度也是最快的(和渲染进程共存,进程关闭就清除缓存,所以有效期比较短)
2.service worker Cache:独立的js线程,可实现离线缓存、消息推送等功能
3.Disk Cache:磁盘中的缓存,容量较大,但是相对的访问速度较慢
4.push Cache:HTTP2
的新特新,优先级最低(前面三种缓存无法命中后才会命中这个)
比如我们现在打开掘金首页的控制台,就可以看到哪些请求是用了缓存的
(红色框里的就是用了memory cache 或disk cache 的)
缓存策略:
浏览器的缓存策略可以分为强缓存 和协商缓存两种
- 强缓存: 不用向服务器请求。可以通过设置
HTTP Header
的expries
和cache-control
实现- expries: 服务器第一次响应时,会将过期的时间戳写在
expries
的请求头中返回,当我们再次请求时,如果本地时间小于expries
的时间戳,就直接去缓存中读取资源- 缺点:如果跟改了本地时间,可能会导致缓存失效
- cache-control: 通过
max-age
控制本地资源的有效期,s-maxage
控制代理服务器(CDN缓存
)的有效期(s-maxage
仅在代理服务器中生效,max-age
仅在客户端生效),设置成no-store
则表示拒绝一切形式的缓存- 缺点:如果更改了本地时间,同样会导致缓存失效,但是解决了
expires
会因为客户端和服务器直接时差不一导致的缓存时间不一致的问题
- 缺点:如果更改了本地时间,同样会导致缓存失效,但是解决了
- expries: 服务器第一次响应时,会将过期的时间戳写在
cache-control
优先级高于expries
协商缓存:浏览器需要先向服务器发起请求,在判断是否从本地获取缓存资源。可以通过设置
HTTP Header
的Last-Modified
和ETag
实现- Last-Modified:首次响应时会返回
Last-Modified
字段,之后客户端每次请求都会带上If-Modified-Since
的字段(上一次response
中的Last-Modified
的值),服务器根据该时间戳和资源在服务器上最后的修改时间是否一致来判断是否使用缓存(返回304
表示使用本地缓存,返回新的Last-Modified
表示不使用缓存)- 缺点:如果服务器没有及时感知到文件的变化(比如在毫秒级时间内完成了改动,但是因为
Last-Modified
是秒级导致无法感知)
- 缺点:如果服务器没有及时感知到文件的变化(比如在毫秒级时间内完成了改动,但是因为
- ETag:既然上面的
Last-Modified
存在着缺点,那ETag
就是他的解决方案(服务器根据文件内容编码生成唯一标识符,可以精确感知文件的变化),首次响应会返回ETag
标识,下一次请求就带上If-None-Match
与服务端进行比较来判断是否使用缓存- 缺点:服务端需要额外的开销来生成
ETag
标识
- 缺点:服务端需要额外的开销来生成
- Last-Modified:首次响应时会返回
Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
HTTP决策
关于HTTP请求的决策,这里我们通过Chrome给出的官方流程图来给大家讲解
接下来我给大家解释下上面的步骤
- 考虑资源是否可复用
- 如果不可以复用,则直接设置
Cache-Control
成no-store
表示拒绝一切形式的缓存 - 如果可以复用,考虑是否设置成每次都需要向浏览器进行缓存的有效确认
- 如果需要,则设置
Cache-Control
为no-cache
- 如果不需要,则考虑是否可以被代理服务器缓存
- 如果可以,设置成
public
- 如果不行,设置成
private
- 如果可以,设置成
- 如果需要,则设置
- 考虑资源有效时长
- 设置对应的
max-age
和s-maxage
等值 - 最后再设置协商缓存需要用到的
ETag
和Last-Modified
- 设置对应的
- 如果不可以复用,则直接设置
以上便是对于一个HTTP请求是否要用缓存以及如何使用缓存的决策过程,我们在考虑是否使用缓存的时候也可以按着这个思路来考虑。
网页刷新对缓存的影响
接下来我们看看网页刷新对各类缓存的影响,帮助大家更好地使用缓存
- 强制刷新: 直接从服务器请求,跳过强缓存和协商缓存(比如
Ctrl + F5
) - 地址栏回车或正常刷新:跳出强缓存,检查协商缓存
- 新窗口的URL访问: 按照正常的缓存策略,即先检查强缓存
相信看到这里你对于如何使用缓存来优化性能应该有了更深入的了解,赶快动手试试吧!
4.使用CDN
说完本地缓存,我们这一节来学一学CDN在前端性能优化上的作用
Q:什么是CDN?
A:CDN是一组分布在不同区域的服务器(内容分发网络),这些服务器上存储着数据的副本,用户访问资源时,可以工具距离来选择较近的服务器,从而达到节省流量,加快访问速度以及减轻网络拥塞的目的。
CDN在前端的作用
CDN在前端有三个核心的作用,即缓存、回源和 前端安全。
1.缓存
前一节我们在学习本地缓存的时候学到,我们的强缓存可以控制我们的资源是否可以被代理服务器缓存(设置成private
),这里的CDN就是常用的代理服务器。
我们经常将一些静态资源放在CDN上 ,比如图片、js资源、css资源等等。
(比如我们打开掘金,就可以看到掘金将很多静态资源都放在了CDN 服务器上来优化)
这里我们可以看到CDN服务器的域名和我们掘金主站的域名并不一样,这样可以解决浏览器对同一域名下的连接数量的限制,这也是使用CDN的一个小细节。
2.回源
故名思意,回源的作用就是当我们的CDN服务器上的资源失效时,向上级服务器请求有效资源的过程。可以配合我们的缓存一起优化性能
3.前端安全
CDN还有一个比较少被提到的作用就是减少网站被攻。 CDN可以通过监控数据、隐藏源IP、过滤黑客入侵以及防止ddos/cc攻击来保护我们的网站
关于CDN的作用暂时就讲这么多,感兴趣的同学可以之后自行查阅
总结
好了,今天的分享就到这里,这几天我会继续更新前端性能优化的其他手段和知识,感兴趣的同学可以点个赞和关注一起学习!!!
下期预告:
- 服务端渲染
- 浏览器底层渲染原理
- 性能监测
- 懒加载和首屏优化
- 防抖节流
- CSS/JS资源的加载
- 权衡优化力度
往期精彩文章: