前端性能优化
前端面试刷题(JS篇):juejin.cn/post/735241…
前端面试刷题必备(手撕代码篇):juejin.cn/post/735607…
前端面试刷题必备(CSS篇):juejin.cn/post/735769…
前端性能优化是一个非常大的体系,可以通过各种各样的方式来实现性能优化,比如:webpack 打包、网络请求、图片压缩、css 优化等等方式。
页面渲染优化
页面渲染的性能优化,大部分人会想到的是输入 URL 到游览器显示页面发生了什么?
这是一个非常大的体系,会从网络请求到游览器渲染,这次我们只讨论游览器渲染的流程:
- 解析 HTML 文件,构建 DOM 树,同事主进程去下载 CSS 文件
- CSS 文件下载完成后,会解析 CSS 文件成树形的数据结构,结合 DOM 树合并成 RenderObject 树
- 这时候会对 RenderObject 树种的元素尺寸、位置等信息进行计算布局
- 开始绘制 RenderObject 树的各个属性,如:背景色,透明度等等
- 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面
上述过程中,我们可以进行那些性能优化?
Script 标签的优化
我们知道在游览器碰到 script 标签时,如果没有defer
和async
,游览器就会立即加载并执行对应的 js 文件,就会造成阻塞。
因此我们可以使用defer
和async
是去异步加载外部的 JS 脚本文件,他们都不会阻塞页面的解析。
defer
和async
他们有啥差别?
- 执行顺序
- 多个带 async 属性的标签,不能保证加载的顺序
- 多个带 defer 属性的标签,按照加载顺序执行
- 脚本是否并行执行
- async 属性表示后续文档的加载和执行与 js 脚本的加载和执行是并行进行的,即异步执行
- defer 属性加载后续文档的过程和 js 脚本的加载时并行进行的,js 脚本需要等元素解析完成后才执行,DOMContentLoaded 时间触发执行之前
游览器的回流和重绘
回流和重绘对游览器的性能消耗都是比较大的,回流必将引起重绘,重绘不一定会引起回流。因为,我们需要尽量避免造成页面的回流。
回流
当元素的尺寸、结构、位置等信息发生变化时,游览器需要重新渲染部分或全部文档,进行重新布局的过程叫做回流。
引起回流的操作有很多:
- 增删 DOM 元素
- 游览器窗口大小变化
- 初次渲染
- 元素字体变化
- 元素尺寸位置变化
…
重绘
当页面中元素的样式属性发生改变但不影响元素的布局位置时,游览器对这个元素的样式重新绘制的过程叫做重绘。
触发重绘的方式也很多:
- 更改字体颜色
- 更改背景色
- 设置透明度
…
使用 RequestAnimationFrame 函数实现动画
页面中可能会存在一些通过 js 实现的动画效果,我们应该避免使用setTimeout()
和setInterval()
来实现,因为这种回调可能会导致丢失帧而发生卡顿。requestAnimationFrame
是浏览器用于定时循环操作的一个接口,类似于setTimeout
,主要用途是按帧对网页进行重绘。显示器有固定的刷新频率(60Hz 或 75Hz),也就是说,每秒最多只能重绘 60 次或 75 次,requestAnimationFrame
的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。
CSS3 动画性能优化
页面中可能存在一些 CSS3 的动画属性,我们就可以考虑使用一些优化手段:
-
创建一个新的渲染层(减少回流)
- 有明确的定位属性(relative\fixed\sticky\absolute)
- 透明度(opacity 小于 1)
- 有 CSS transfrom 属性(不为 none)
- 当前有对于 opacity\transform\fliter\backdrop-filter 属性的动画
-
创建合成层。合成层会开始 GPU 加速页面渲染,但不能滥用
- 对 opacity\transform\fliter\backdrop-filter 应用了 animation 或 transition(需要时 active 的 animation 或者 transition)
- will-change 设置为 opacity\transform\top\left\bottom\right;(提前通知浏览器元素将要做什么动画,让浏览器提前准备合适的优化设置)
- 有 3D transform 函数:比如 translate3d\scale3d\rotate3d 等
图片优化
图片压缩
其实就是减少图片的体积大小,提高网络请求速度。
精灵图
就是像一些 icon 小图片,我们可以将这些图片集成在一张图片里,通过定位等相关技术实现展示需要的图标。
SVG 替换图片
SVG 是可缩放矢量图形。(矢量:既有大小又有方向的量),可以按比例缩小,并支持压缩。
优点:
- 放大缩小不会失真,可被许多设备和游览器中使用。
- 灵活,可以直接使用 js 和 css 操作
- 可移动话,可以使用 js\css\smil 等方式进行动画
- 轻量级,与其他格式相比,svg 文件通常较小
- 可打印,可以以任何分辨率打印不损失图像质量
- 利于 SEO,因为 svg 文件是 xml 格式的,可以被搜索引擎索引
- 可压缩,支持压缩
- 易于编辑
缺点:
- 不适合高清图片制作,适合用于图标,无法显示与标准图片格式一样多的细节
- 变得复杂时,加载会比较慢
- 不完全扩平台
和 canvas 的区别:
- 可扩展性:
- svg 是基于矢量的点、线、形状和数学公式来构建的图形,放大缩小不会失真
- Canvas 是由一个个像素点构成的图形,放大会模糊
- SVG 可以再任何分辨率下高质量打印,canvas 不行
- 渲染能力:
- SVG 很复杂的时候渲染能力会变得很慢,因为很大程度上去使用 DOM
- Canvas 渲染能力很强,适合图像密集型的游戏开发
- 当图像有大量元素的时候,SVG 文件的大小会增加很快,Canvas 则不会
- 灵活度:SVG 可以通过 js 和 css 进行修改,Canvas 只能通过 js 脚本操作
图片懒加载
如果页面中存在大量的图片,一次性全部加载就会变的很慢,因此我们可以让页面先加载一个占位图,然后游览页面的时候,随着可视区域的变化,将原先的占位图替换成真实的图片。
能有效地提高页面加载速度,并且减少服务器负载。
实现方案一:
var imgs = document.querySelectorAll("img");
//offsetTop是元素与offsetParent的距离,循环获取直到页面顶部
function getTop(e) {
var T = e.offsetTop;
while ((e = e.offsetParent)) {
T += e.offsetTop;
}
return T;
}
function lazyLoad(imgs) {
var H = document.documentElement.clientHeight; //获取可视区域高度
var S = document.documentElement.scrollTop || document.body.scrollTop;
for (var i = 0; i < imgs.length; i++) {
if (H + S > getTop(imgs[i])) {
imgs[i].src = imgs[i].getAttribute("data-src");
}
}
}
window.onload = window.onscroll = function () {
//onscroll()在滚动条滚动的时候触发
lazyLoad(imgs);
};
实现方案二:
var imgs = document.querySelectorAll("img");
//用来判断bound.top<=clientHeight的函数,返回一个bool值
function isIn(el) {
var bound = el.getBoundingClientRect();
var clientHeight = window.innerHeight;
return bound.top <= clientHeight;
}
//检查图片是否在可视区内,如果不在,则加载
function check() {
Array.from(imgs).forEach(function (el) {
if (isIn(el)) {
loadImg(el);
}
});
}
function loadImg(el) {
if (!el.src) {
var source = el.dataset.src;
el.src = source;
}
}
window.onload = window.onscroll = function () {
//onscroll()在滚动条滚动的时候触发
check();
};
WebP
将图片转换为 webp 格式,减少体积提高速度。
节流与防抖
代码实现参考:juejin.cn/post/735607…
防抖
防抖是在时间被触发 n 秒后,再执行回调,在这 n 秒内如果再被触发,则重新计时,像一些点击事件产生的网络请求等等。
防抖的应用场景:
- 按钮提交,避免多次点击造成多次提交
- 文本输入查找,避免造成过多的请求
节流
节流则是在一个规定的时间内,只会触发一次事件的回调,像我们的页面滚动等等,可以用节流来控制事件调用的频率。
节流的应用场景:
- 拖拽:固定时间内执行一次回调,避免超高频率执行
- 页面滚动:同上
- 缩放:监控游览器页面变化
打包优化
在我们开发中,我们有很多的打包工具可以使用,像一些 webpack、vite、Gulp 等等,在这里我们以 webpack 为例。
既然说到了打包,那我们先来简单了解一下打包的流程:
webpack 的运行是一个串行的过程
- 初始化参数:从配置文件或 shell 语句中读取合并参数,得到最终的参数
- 开始编译:用上一步得到的参数初始化
Compiler 对象
,加载所有配置的插件,执行 run 方法开始编译。 - 确定入口:根据配置文件中的
entry 参数
找到所有的入口文件。 - 编译模块:从入口文件出发,调用所有配置的
Loader
对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。 - 完成模块编译:在经过第 4 步使用
Loader
翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。 - 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。 - 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
总结就是 3 个阶段:
- 初始化:启动构建,读取与合并配置参数,加载
Plugin
,实力化Compiler
- 编译:从 Entry 触发,针对每一个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出:将编译以后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中
那我们如何利用 webpack 提高打包效率呢,我们此处列举部分内容
webpack 优化-多进程打包
当我们的项目体量逐渐变大的时候,打包就会变得非常漫长,因为他是单线程模式的,只能逐个文件处理。因此开启多进程去打包是非常有必要的。常见的 loader 有thread-loader
使用:只要把 thread-loader
放置在其他 loader 之前, 那 thread-loader
之后的 loader 就会在一个单独的 worker 池(worker pool)
中运行。
webpack 优化-利用缓存(缩短连续构建时间)
webpack 缓存的方法有很多,比如cache-loader
、HardSourceWebpackPlugin
、babel-loader
的cacheDirectory
标志。这些都可以在重新运行期间节约大量的时间,但是在初次运行的时候会比较慢。
cache-loader
使用起来比较简单,在一些性能开销大的 loader 之前添加此 loader
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ["cache-loader", ...loaders],
include: path.resolve("src"),
},
],
},
};
HardSourceWebpackPlugin
第一次构建花费正常的时间,第二次构建显著加快
const HardSourceWebpackPlugins = require("hard-source-webpack-plugin");
const clientWebpackConfig = {
// ...
plugins: [
new HardSourceWebpackPlugin({
// cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
// 'node_modules/.cache/hard-source/[confighash]'
cacheDirectory: path.join(
__dirname,
"./lib/.cache/hard-source/[confighash]"
),
// configHash在启动webpack实例时转换webpack配置,
// 并用于cacheDirectory为不同的webpack配置构建不同的缓存
configHash: function (webpackConfig) {
return require("node-object-hash")({ sort: false }).hash(webpackConfig);
},
// 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
// hard-source需要替换缓存以确保输出正确。
// environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
environmentHash: {
root: process.cwd(),
directories: [],
files: ["package-lock.json", "yarn.lock"],
},
info: {
mode: "none",
level: "debug",
},
cachePrune: {
maxAge: 2 * 24 * 60 * 60 * 1000,
sizeThreshold: 50 * 1024 * 1024,
},
}),
new HardSourceWebpackPlugins.ExcludeModulePlugin([
{
test: /.*\.DS_Store/,
},
]),
],
};
webpack 优化-HotModuleReplacement
HMR 也就是热模块替换:在程序运行中,替换、添加、删除模块,而无需重新加载整个页面,从而提高了开发时的构建速度。
devServer: {
static: {
directory: path.join(__dirname, '../public'), // 通过 static.directory 配置项告诉 dev-server 监听文件。默认启用,文件更改将触发整个页面重新加载。可以通过将 watch 设置为 false 禁用。
},
client:{
progress: true, // 在游览器中以百分比显示编译进度
},
hot: true, // 启用webpack的热模块替换
compress: true, // 启用 gzip compression
host:"localhost", // 启动服务器域名
port: 9000, // 启动服务器端口号
open:true, // 自动打开浏览器
}
webpack 优化-减少代码体积 Tree Shaking
为什么?
开发的时候我们定义了一些工具函数库,或者引入第三方工具函数库或组件。如果没有处理的话,打包时会引入整个库,但是实际上我们可能只是用上极小部分的功能。
这样将整个库都打包进去,体积就太大了。
Tree Shaking
是一种术语,通常用于描述一处 Javascript 中没有用上的代码。webpack 已经默认开启该功能,无需其他配置。
注意:它依赖 ES Module
webpack 打包优化总结
我们从 4 个角度对 webpack 和代码进行了优化:
- 提升开发体验
- 使用 Source Map 让开发或上线时代码报错能有更加准确的错误提示。
- 提升 webpack 提升打包构建速度
- 使用 HotModuleReplacement 让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
- 使用 OneOf 让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
- 使用 Include/Exclude 排除或只检测某些文件,处理的文件更少,速度更快。
- 使用 Cache 对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
- 使用 Thead 多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
- 减少代码体积
- 使用 Tree Shaking 剔除了没有使用的多余代码,让代码体积更小。
- 使用 @babel/plugin-transform-runtime 插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
- 使用 Image Minimizer 对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
- 优化代码运行性能
- 使用 Code Split 对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
- 使用 Preload / Prefetch 对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
- 使用 Network Cache 能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
- 使用 Core-js 对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
- 使用 PWA 能让代码离线也能访问,从而提升用户体验。
后续网络请求的优化以及框架库的优化放置其他章节进行讲述。
原文链接:https://juejin.cn/post/7357909187953311785 作者:沸羊羊你快用力推呀