性能优化
性能优化
1. 从输入URL到页面加载完成,发生了什么
2. webpack与Gzip
3. 图片优化
4. 浏览器缓存
5. 本地存储
6. CDN
7. 服务端渲染
8. 浏览器端的性能优化
9. DOM优化
10. 事件循环与异步更新策略
11. 回流与重绘
12. 优化首屏体验-懒加载
13. 节流与防抖
14. 性能监测
从输入URL到页面加载完成,发生了什么
-
用户输入URL
-
浏览器解析URL
- 浏览器内部代码会解析这个URL。它首先会检查本地hosts文件,看是否有对应的域名。如果有,浏览器就会直接向该IP地址发送请求。如果没有,浏览器会将域名发送给DNS服务器进行解析,将域名转换成对应的服务器IP地址。
- 建立TCP连接(三次握手)
- 浏览器得到IP地址后,会通过TCP协议与服务器建立连接。TCP/IP协议是Internet的基础,它负责确保数据在网络中的可靠传输。这一过程中会进行三次握手,确保双方都已准备好进行通信。
- 发送HTTP请求
- TCP连接建立后,浏览器会向服务器发送HTTP请求。这个请求包含了请求行(如GET方法、请求的URI、HTTP版本等)、请求头部(如Accept-Charset、Accept-Encoding等)以及可能存在的请求正文。
- 服务器处理请求
- 服务器收到请求后,会根据请求的内容进行相应的处理。这可能包括查询数据库、生成动态内容等。
- 发送HTTP响应
- 服务器处理完请求后,会发送一个HTTP响应给浏览器。这个响应包含了状态行(如HTTP版本、状态码、状态描述等)、响应头部(如Content-Type、Content-Length等)以及响应正文(即实际要显示的页面内容)。
- 浏览器解析和渲染页面
- 浏览器收到响应后,会解析响应正文中的HTML代码,并下载所需的CSS、JavaScript等资源文件。然后,浏览器会根据这些资源来渲染页面,最终将页面呈现给用户。
- TCP连接关闭(四次挥手)
- 浏览器解析渲染页面
- 解析文档,生成DOM树;
- 解析css,根据css规则生成CSSOM规则树;
- 在CSSOM树和DOM树生成完后,合并DOM、CSSOM树构建渲染树;
- 渲染树构建完成后,开始计算元素大小和位置;
- 根据计算好的位置信息将内容渲染到屏幕上;
- 通过URL页面加载考虑性能优化的点,如网络层面的优化可考虑DNS解析使用DNS缓存,TCP连接使用长连接,HTTP请求优化减少请求次数和请求体积,请求最近服务器上的资源;浏览器端的优化可考虑资源加载优化、服务端渲染、浏览器缓存机制的利用、DOM 树的构建、网页排版和渲染过程、回流与重绘的考量、DOM 操作的合理规避等;
webpack与Gzip
- 从URL到显示页面这个过程,涉及到网络层面的有3个主要过程:DNS解析、TCP连接、HTTP请求/响应;对于DNS解析和TCP连接两个步骤,前端可以做的努力有限。HTTP连接层面的优化是网络优化的核心。
- HTTP优化的两个大的方向:减少请求次数、减少单次请求所花费的时间;考虑资源的压缩与合并,也是构建工具在做的事情,如webpack;
- webpack性能优化的瓶颈:webpack的构建过程太花时间;webpack打包的结果体积太大;
webpack优化方案
- 构建过程提速
- 减少loader事情,如include或exclude避免不必要的转译;开启缓存将转译结果缓存至文件系统,cacheDirectory=true;
- 不要放过第三方库,以node_modules为代表,文件很大又不可或缺;处理第三方库很很多种方式,如DllPlugin只有当依赖自身发生版本变化时才会重新打包;
- 将loader由单进程转为多进程;HappyPack、thead-loader;
- 构建结果体积压缩
- 移除 console;
- 拆分资源;
- 删除冗余代码,如Tree-Shaking,UglifyJsPlugin,适合用来处理模块级别的冗余代码;至于粒度更细的冗余代码的去除,往往会被整合进js或css的压缩或分离过程中;
- 按需加载,
require.ensure
在正确的时机去触发相应的回调;- 多线程压缩TerserPlugin
- Gzip压缩:在一个文本文件中找出一些重复出现的字符串,临时替换它们,从而使整个文件变小;
- 开启Gzip只需要在request headers中加一句话:
accept-encoding:gzip
- HTTP压缩,以缩小体积为目的,对HTTP内容进行重新编码的过程;
图片优化
- JPEG、JPG,
- 有损压缩、体积小、加载快,不支持透明;
- 大的背景图、轮播图或Banner图出现;使用 JPG 呈现大图,既可以保住图片的质量,又不会带来令人头疼的图片体积;
- 当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显;
- JPEG 图像不支持透明度处理;
- PNG-8/PNG-24,
- 无损压缩、质量高、体积大、支持透明;
- 呈现小的 Logo、颜色简单且对比强烈的图片或背景等;
- SVG,
- 文本文件、体积小、不失真、兼容性好;
- SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述,是一种基于XML语法的图像格式;
- 局限性:渲染成本比较高,不利于性能;可编程的,学习成本大;
<!-- 将svg写入html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<circle cx="50" cy="50" r="50" />
</svg>
</body>
</html>
<!-- 将svg写入独立文件后引入html -->
<img src="文件名.svg" alt="">
- Base64,
- 文本文件、依赖编码、小图标的解决方案;
- Base64 和雪碧图,是作为小图标解决方案而存在的;相较于一个小图标一个图像文件,单独一张图片所需的 HTTP 请求更少,对内存和带宽更加友好。
- Base64 是作为雪碧图的补充而存在的;
- Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,可直接将编码结果写入 HTML 或者写入 CSS,减少 HTTP 请求的次数。
- 缺陷:base64编码之后,图片大小会膨胀为原来的4/3,所以不适合大背景图片;
- 使用base64:图片的实际尺寸很小;图片无法以雪碧图的形式与其他小图结合(合成雪碧图仍是主要的减少http请求的途径,base64是雪碧图的补充);图片的更新频率非常低;
- 利用 webpack 来进行 Base64 的编码,url-loader,具备base64转码能力,还可以结合文件大小,判断图片是否有必要进行base64编码;
- WebP,
- 年轻的全能型选手;像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身;
- 加快图片加载速度的图片格式,支持有损压缩和无损压缩;
- 缺点:年轻,浏览器兼容性问题;
<!-- 维护性更强、更加灵活的方案——把判断工作交给后端,由服务器根据 HTTP 请求头部的 Accept 字段来决定返回什么格式的图片。当 Accept 字段包含 image/webp 时,就返回 WebP 格式的图片,否则返回原图 -->
<img src="//img.alicdn.com/tps/i4/TB1CKSgIpXXXXccXXXX07tlTXXX-200-200.png_60x60.jpg_.webp" alt="手机app - 聚划算" class="app-icon">
浏览器缓存
- 缓存可以减少网络 IO 消耗,提高访问速度。
- 浏览器缓存按照获取资源时请求的优先级顺序分为:Memory Cache、Service Worker Cache、HTTP Cache、Push Cache,可以从请求的network的Size一栏看出是哪种缓存;
HTTP缓存:强缓存、协商缓存;
- 强缓存
- 利用http头中的Expires和Cache-Control(max-age/s-maxage)两个字段来控制的。
- 强缓存中,当请求再次发出时,浏览器会根据其中的expires和cache-control判断目标资源是否命中缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信;命中强缓存返回200(disk cache);
- Cache-Control 的 max-age 配置项相对于 expires 的优先级更高。当 Cache-Control 与 expires 同时出现时,以 Cache-Control 为准。
cache-control: max-age=3600, s-maxage=31536000
s-maxage 优先级高于 max-age,两者同时出现时,优先考虑 s-maxage(代理服务器的缓存问题)。如果 s-maxage 未过期,则向代理服务器请求其缓存内容。
- 协商缓存:
- 依赖于服务端与浏览器之间的通信;
- 协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
- 如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304;
- Last-Modified、if-None-Match,存在不能正确感知文件变化的情况,常见的两个场景:(1) 我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求;(2)当我们修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了;
- Etag,由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,能够精准地感知文件的变化;需要服务器额外付出开销,会影响服务端的性能;
- HTTP缓存决策
- 首先,当资源内容不可复用时,Cache-Control设置为no-strore.拒绝缓存;
- 其次,考虑是否需要每次向服务器进行缓存有效确认,需要则设置Cache-Control为no-cache,不需要确认则考虑该资源是否可以被代理服务器缓存,根据其结果决定是设置为private还是public;
- 然后,考虑该资源的过期时间,设置对应的max-age和s-maxage值;
- 最后设置协商缓存需要用到的Etag、Last-Modified等参数;
MemoryCache:存在内存中的缓存;
- 存在内存中的缓存;
- 从优先级上来说,它是浏览器最先尝试去命中的一种缓存;从效率上来说,它是响应速度最快的一种缓存;
- Base64 格式的图片,几乎永远可以被塞进 memory cache;
- 当进程结束后,即tab关闭后,内存里的数据也不复存在;
Service Worker Cache
- 一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM;
- Service Worker可以帮助我们实现离线缓存、消息推送、网络代理等功能;借助Service worker实现的离线缓存被称为Service Worker Cache;
- Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。
- Server Worker 对协议是有要求的,必须以 https 协议为前提
Push Cache
- Push Cache 是指 HTTP2 在 server push 阶段存在的缓存;
- Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
- Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
- 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
本地存储
- Cookie、Web Storage、IndexedDB
- Cookie
- 有体积上限的,它最大只能有 4KB;
- 通过响应头里的 Set-Cookie 指定要存储的 Cookie 值。默认情况下,domain 被设置为设置 Cookie 页面的主机名,我们也可以手动设置 domain 的值:
Set-Cookie: name=xiuyan; domain=xiuyan.me
; 同一个域名下的所有请求,都会携带 Cookie;
- Web Storage:
- 为浏览器存储而提供的数据存储机制, Local Storage 与 Session Storage,二者的区别在于生命周期和作用域的不同;
- 存储容量大, 5-10M;
- 仅位于浏览器端,不与服务端发生通信;
- Local Storage:持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;适合存储一些内容稳定的资源,如bse64图片字符串、不经常更新的css、js静态资源;
- Session Storage:Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。适合用来存储生命周期和它同步的会话级别的信息;
// 存储数据
localStorage.setItem('user_name', 'aa')
// 读取数据
localStorage.getItem('user_name')
// 删除某一键值数据
localStorage.removeItem('user_name')
// 清空数据记录
localStorage.clear()
- IndexedDB
- 一个运行在浏览器上的非关系型数据库;
CDN
- CDN 往往被用来存放静态资源 (像 JS、CSS、图片等不需要业务服务器进行计算即得的资源);静态资源访问频率高、承接流量大,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN是静态资源提速的重要手段。
- CDN核心点:缓存、回溯;
- 缓存:把资源copy一份到CDN服务器上的过程;
- 回溯:CDN发现自己没有需要的资源(一般是缓存过期),向它的上层服务器/根服务器去要该资源的过程;
- 把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现!
服务端渲染SSR
- 本质:本该浏览器做的事情,分担给服务器去做;
- 是什么 服务端渲染的运行机制
- 客户端渲染模式:服务端会把渲染需要的静态文件发给客户端,客户端加载过来之后,自己在浏览器里跑一遍JS,根据JS的运行结果,生成相应的DOM;这样页面上呈现的内容,在html源文件里找不到;
- 服务端渲染模式:当用户第一次请求页面时,由服务器把需要的组件或页面渲染成HTML字符串,然后把它返回给客户端;客户端拿到手的是可以直接渲染然后呈现给用户的HTML内容,不需要为了生成DOM再跑一遍JS;使用服务端渲染即所见即所得,页面上呈现的内容,我们在html源码文件里也能找到。
- 为什么 服务端渲染解决了什么性能问题
- 使用服务端渲染之后,利于搜索引擎搜索出关键词;且解决了首屏加载慢的问题;(客户端渲染模式下,除了加载HTML,还要等渲染所需的JS加载完,还要把这部分JS在浏览器上再跑一遍)
- 怎么做 服务端渲染的应用实例与使用场景
- renderToString() 将虚拟DOM转化为真实DOM;
- 把转化结果塞进模板里;
// vue,服务端入口文件
const Vue = require('vue')
// 创建一个express应用
const server = require('express')()
// 提取出renderer实例
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
// 编写Vue实例(虚拟DOM节点)
const app = new Vue({
data: {
url: req.url
},
// 编写模板HTML的内容
template: `<div>访问的 URL 是: {{ url }}</div>`
})
// renderToString 是把Vue实例转化为真实DOM的关键方法
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
// 把渲染出来的真实DOM字符串插入HTML模板中
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
浏览器端的性能优化
- 浏览器内核可以分成两部分:渲染引擎、JS引擎;
- 渲染引擎包括:HTML解释器、CSS解释器、布局、网络、存储、图形、音视频、图片解码器等零部件;
- 浏览器内核分类:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)
- Blink基于 Webkit 衍生而来的一个分支
- DOM树:接续HTML以创建的是DOM树,渲染引擎开始解析HTML文档,转换树转换的标签到DOM节点,它被称为DOM树;
- CSSOM树:解析css创建的是CSSOM树,CSSOM的解析过程与DOM树的解析过程是并行的;
- 渲染树:CSSOM与DOM结合,得到的是渲染树;
- 布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,便得到了基于渲染树的布局渲染树;
- 绘制渲染树:遍历渲染树,每个节点将使用UI后端层来绘制,整个过程叫做绘制渲染树;
- 渲染过程:首先基于HTML构建一个DOM树,这棵DOM树与CSS解释器解析出的CSSOM相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,我们页面的初次渲染就完成了。之后每当一个新元素加入到这个DOM树当中,浏览器便会通过CSS引擎查遍CSS样式表,找到符合该元素的样式规则应用到这个元素上,然后再重新去绘制它。
- 查表是个花时间的活,怎么让浏览器的查询工作又快又好地实现呢?第一个可转化为代码的优化点——CSS 样式表规则的优化.
CSS 样式表规则的优化
-
样式查找:CSS引擎查找样式表,每条规则都是从右向左匹配,
#mylist li {}
这样会先匹配到li,查找所有li,再对li父元素的id进行匹配,这样li过多的情况下就会造成性能浪费;* {}
通配符会匹配所有元素;基于此,可考虑:(1)避免使用通配符,只对需要用到的元素进行选择; (2)关注可以通过继承实现的属性,避免重复匹配重复定义; (3)少用标签选择器,如果可以,可以用类代替;(4)不要画蛇添足,id和class选择器不应该被多余的标签选择器拖后腿,如.myList#title
,可#title
。(5)减少嵌套,后代选择器的开销是最高的,尽量将选择器的深度降到最低,尽量不要超过三层,尽可能使用类来关联每一个标签元素; -
告别阻塞:CSS与JS的加载顺序优化
- CSS阻塞:将 CSS 放在 head 标签里;启用 CDN 实现静态资源加载速度的优化;
- JS阻塞:通过对js使用 defer 和 async 来避免不必要的阻塞;
- JS的三种加载方式:
- 正常模式
<script src="index.js"></script>
这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。- async模式
<script async src="index.js"></script>
async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。- defer模式
<script defer src="index.js"></script>
defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。
DOM优化
- DOM优化思路:减少 DOM 操作,一方面是dom渲染影响性能,另一方面就是js引擎和渲染引擎交流需要成本;减少 DOM 操作的核心思路,就是让 JS 去给 DOM 分压
- 异步更新策略
- 回流与重绘
- 对DOM的修改触发了渲染树的变化,就会触发回流或重绘。
- 回流:当对DOM的修改引发DOM几何尺寸的变化如修改元素的宽、宽、位置、隐藏元素时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来。这个过程叫回流,也叫重排。
- 重绘:当对DOM的修改导致了样式的变化,但并未影响其几何属性,如修改颜色或背景色,浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式。这个过程叫重绘。
- 重绘不一定会导致回流;回流一定会导致重绘;
// 每一次循环都调用 DOM 接口重新获取了一次 container 元素,相当于每次循环都交了一次过路费。前后交了 10000 次过路费,但其中 9999 次过路费都可以用缓存变量的方式节省下来
for(var count=0;count<10000;count++){
document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'
}
// 缓存变量
// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){
container.innerHTML += '<span>我是一个小测试</span>'
}
// 上面代码不必要的 DOM 更改太多了
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){
// 先对内容进行操作
content += '<span>我是一个小测试</span>'
}
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content
// 字符串变量 content 就扮演着一个 DOM Fragment 的角色。其实无论字符串变量也好,DOM Fragment 也罢,它们本质上都作为脱离了真实 DOM 树的容器出现,用于缓存批量化的 DOM 操作。
let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是一个小测试'
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)
事件循环与异步更新策略
浏览器中的事件循环
- JS分为同步任务和异步任务;
- 同步任务都在主线程(JS引擎线程)上执行,会形成一个执行栈;主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调;一旦执行栈中的所有同步任务执行完毕(JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行;
- 事件循环中的异步队列有2种:宏任务队列、微任务队列
- 常见的宏任务:主代码块、setTimeout、setInterval、setImmediate、script整体代码、I/O操作、requestAnimationFrame 、UI渲染等;
- 常见的微任务: process.nextTick、Promise.then、catch、finally、Object.observe、MutationObserver 等;
- 浏览器会先执行一个宏任务,紧接着执行当前执行栈产生的微任务,再进行渲染,然后再执行下一个宏任务;
- 微任务和宏任务不在一个任务队列;如setTimeout是一个宏任务,它的事件回调在宏任务队列,Promise.then()是一个微任务,它的事件回调在微任务队列;
- 一个完整的事件循环过程:
- 首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分;
- 同步任务会直接进入主线程依次执行;
- 异步任务会再分为宏任务和微任务;
- 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
- 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
- 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
- 上述过程会不断重复,这就是Event Loop,比较完整的事件循环;
- 每一个循环都是这样一个过程:将一个macro-task执行并出队=》将一队micro-task执行并出队=》执行渲染操作,更新界面=》处理下一个macro-task;
- 更新 DOM 的时间点,尽可能靠近渲染的时机。当需要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。
Promise.resolve().then(task)
- 异步更新:更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发,这就是异步更新。异步更新可以帮助我们避免过度渲染。只看结果。
回流与重绘
- 回流:当对DOM的修改引发了DOM几何尺寸(如修改元素的宽高位置或隐藏元素)的变化时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来,这个过程叫回流。
- 重绘:当对DOM的修改导致了样式的变化,却并未影响其几何属性时,如修改颜色背景色,浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式,这个过程叫重绘。
- 重绘不一定会导致回流,回流一定会导致重绘。
- 要避免回流与重绘的发生,最直接的做法是避免掉可能会引发回流与重绘的 DOM 操作;
- 触发重绘的操作:背景色、文字色、可见性(这里指visibility:hidden这样不用=改变元素位置和存在性的,单纯针对可见性的操作)
- 触发回流的操作:改变DOM元素的几何属性(width/height/padding/margin/left/top/border等);改变DOM树的结构(增减、移动等操作);获取一些特定属性(offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight)的值,这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流、当调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。
- 如何规避回流与重绘:
- 将敏感操作缓存起来,避免频繁改动
// 获取el元素
const el = document.getElementById('el')
// 这里循环判定比较简单,实际中或许会拓展出比较复杂的判定需求
for(let i=0;i<10;i++) {
el.style.top = el.offsetTop + 10 + "px";
el.style.left = el.offsetLeft + 10 + "px";
}
// 改成
// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el')
let offLeft = el.offsetLeft, offTop = el.offsetTop
// 在JS层面进行计算
for(let i=0;i<10;i++) {
offLeft += 10
offTop += 10
}
// 一次性将计算结果应用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop + "px"
- 避免逐条改变样式,使用类名去合并样式
const container = document.getElementById('container')
// 每次单独操作,都去触发一次渲染树更改,从而导致相应的回流与重绘过程。
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
// 改成
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
const container = document.getElementById('container')
// 将所有的更改一次性发出,用一个 style 请求解决掉了
container.classList.add('basic_style')
- 将DOM离线,将元素设置为display:none,将其从页面上拿掉,后续再操作将无法触发回流与重绘;
// DOM离线前
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...
// DOM离线后
let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...
container.style.display = 'block'
- flush队列
- 下面代码在现代浏览器中只进行了一次回流和一次重绘,和上面的知识点有出入,原因在于:浏览器自己缓存了一个flush队列,把触发的回流和重绘任务都塞进去,待JavaScript执行栈清空时或者不得已时将这些任务一次性出队。
- 不得已的时机:获取一些特定属性(offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight)的值,这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流;
let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
优化首屏体验-懒加载
- 懒加载:主要用于图片、视频或其他大型资源的加载;原理是:当用户滚动页面到特定位置时再去加载相应的资源,而不是在页面初始加载时就一次性加载所有资源。
- 图片懒加载,针对图片加载时机的优化,通常是通过监听滚动事件或使用 Intersection Observer API 来判断图片是否进入了可视区域,如果进入则加载图片。这样可以减少初始页面加载时间,提高页面加载速度,节省带宽,提升用户体验。
- 在视频方面,懒加载可以避免在页面加载时就预加载整个视频,而是在用户点击播放或视频即将进入可视区域时再加载。
- 在懒加载的实现中,有两个关键的数值:一个是当前可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
;一个是元素距离可视区域顶部的高度getBoundingClient().top
;
- 给需要懒加载的图片设置一个自定义属性,例如 data-src 来存储图片的真实 URL,而初始的 src 属性可以设置为一个占位图片或空白。
- 在页面加载时,获取所有需要懒加载的图片元素。
- 为滚动事件添加监听函数。
- 在监听函数中,判断图片是否进入了可视区域。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Lazy-Load</title>
<style>
.img {
width: 200px;
height:200px;
background-color: gray;
}
.pic {
// 必要的img样式
}
</style>
</head>
<body>
<div class="container">
<div class="img">
<!-- 注意我们并没有为它引入真实的src -->
<!-- data-src 来存储图片的真实 URL,而初始的 src 属性可以设置为一个占位图片或空白 -->
<img class="pic" alt="加载中" data-src="./images/1.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/2.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/3.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/4.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/5.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/6.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/7.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/8.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/9.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/10.png">
</div>
</div>
</body>
</html>
// 获取所有的图片标签
const imgs = document.getElementsByTagName('img');
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0;
function lazyload() {
for (let i = num; i < imgs.length; i++) {
// 用可视区域高度减去元素顶部距离可视区域顶部的高度
let distance = viewHeight - imgs[i].getBoundingsClientRect().top;
// 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
if (distance >= 0) {
// 给元素写入真实的src,展示图片
imgs[i].src = imgs[i].getAttribute('data-src');
// 前i张图片加载完毕,下次从第i+1张开始检查是否露出
num = i + 1;
}
}
}
// 监听scroll事件,false 参数表示事件处理函数在冒泡阶段执行
window.addEventListener('scroll', lazyload, false);
// 用户的每一次滚动都将触发我们的监听函数。函数执行是吃性能的,频繁地响应某个事件将造成大量不必要的页面计算。针对那些有可能被频繁触发的事件作进一步地优化——throttle 与 debounce。
节流与防抖
- 类似scroll事件、rsize事件、鼠标事件如mousemove/mouseover、键盘事件如keyup/keydown,都存在被频繁触发的风险;
- 频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿,需要一些手段来控制事件被触发的频率,由此出现了事件节流throttle和事件防抖debounce;
- 节流与防抖的本质:都以闭包形式存在;通过对事件对应的回调函数进行包裹,以自由变量的形式缓存时间信息,最后用setTimeout来控制事件的触发频率;
- 节流throttle:第一个人说了算;在某段事件内,不管你触发了多少次回调,都只认第一次,并在计时结束时给与响应;
// fn是我们需要包装的事件回调,interval是时间间隔的阈值
function throttle(fn, interval) {
// last为上一次触发回调的时间
let last = 0;
// 将throttle处理结果当作按时返回
return function () {
// 保留调用时的this上下文
let context = this;
// 保留调用时传入的参数
let args = arguments;
// 记录本次触发回调的时间
let now = +new Date();
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last >= interval) {
// 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
last = now;
fn.apply(context, args);
}
}
}
// 使用throttle来包装scoll的回调
const better_scroll = throttle(() => console.log('scroll---'), 1000);
document.addEventListener('scroll', better_scroll);
- debounce:最后一个人说了算;在某段时间内,不管你触发了多少次回调,都只认最后一次;
- 如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。
// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
// 定时器
let timer = null;
// 将debounce处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this;
// 保留调用时传入的参数
let args = arguments;
// 每次事件被触发时,都去清除之前的旧定时器
if (timer) {
clearTimeout(timer)
}
// 设立新定时器
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
const better_scroll = debounce(() => console.log('scroll--'), 1000);
document.addEventListener('scroll', better_scroll);
- 在throttle的逻辑里,“第一个人说了算”,它只为第一个乘客计时,时间到了就执行回调。而 debounce 认为,“最后一个人说了算”,debounce 会为每一个新乘客设定新的定时器。
- 用throttle优化debounce
// fn是需要包装的事件回调,delay是时间间隔的阈值
function throttle(fn, delay) {
// last为上一次触发回调的事件,timer是定时器
let last = 0, timer = null;
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this;
// 保留调用时传入的参数
let args = arguments;
// 记录本次触发回调的时间
let now = +new Date();
// 判断上次触发的时间额本次触发的时间差是否小于时间间隔的阈值
if (now - last < delay) {
// 如果时间间隔小于设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
clearTimeout(timer);
timer = setTimeout(function () {
last = now;
fn.apply(context, args)
}, delay)
} else {
// 如果时间间隔超出了设定的时间间隔阈值,那就直接执行一次反馈给用户
last = now;
fn.apply(context, args)
}
}
}
性能监测
- 监测的目的:确定性能瓶颈,优化;
- 性能监测方案:可视化方案、可编程方案
- Performance,Chrome 开发者工具,用于记录和分析应用在运行时的所有活动。它呈现的数据具有实时性、多维度的特点,可以定位性能问题。
- 可视化监测:Lighthouse,chrome插件;可生成性能报告;
- 可编程的性能上报:
window.performance
参考&感谢各路大神
前端性能优化原理与实践
JS运行机制
HTTP及浏览器篇
宝剑锋从磨砺出,梅花香自苦寒来。