图像优化
1.图像的选取与使用
矢量图与位图
- 矢量图:SVG,比如Logo、字体、小图标,优点:缩放不会影响清晰度 缺点:不能显示太多信息,只适合简单的图标
- 位图:也就是我们常说的图片,缺点:放大后会显示很多像素块,文件较大
图片格式:
- JPEG:有损压缩,适合与对图片质量要求不高的图片 基线压缩:图片加载从上至下,每次加载一部分,直到整个图片完全加载 渐进式压缩:首先展示出一个完整的但模糊的图片,每次加载一部分像素,直至图片清晰
- GIF:动画,体积会很大
- PNG:无损压缩,支持透明度,图片体积较大
- WebP
- Base64: 本质上不是图片,采用Base64编码图片时,无需继续向图片服务器发请求,直接解析编码即可,但缺点就是经过Base64编码后的图片,体积会变大,对于小图片还能接受,但大图片来说最好不用base64编码
图片使用策略
雪碧图
加载优化
图片延迟加载
传统方式:监听鼠标scroll事件、窗口resize事件来判断图片是否在可视区域、动态修改img的src
思路:先为图片的src设置一个占位图片,将真实的图片地址放在自定义属性中,以便后续在事件中修改,当图片出现在可视区域或将要出现在可视区域时,用js将图片的src替换为真实地址,发送http请求,当所有的图片都加载完成,则移除鼠标scroll事件、窗口resize事件。
<img class="lazy" src="/img/placeholder.jpg"
data-src="http://e.hiphotos.baidu.com/image/pic/item/a1ec08fa513d2697e542494057fbb2fb4316d81e.jpg" width="1500px"
height="1000px">
<img class="lazy" src="/img/placeholder.jpg"
data-src="http://c.hiphotos.baidu.com/image/pic/item/30adcbef76094b36de8a2fe5a1cc7cd98d109d99.jpg"
width="1500px"
height="1000px">
<img class="lazy" src="/img/placeholder.jpg"
data-src="http://h.hiphotos.baidu.com/image/pic/item/7c1ed21b0ef41bd5f2c2a9e953da81cb39db3d1d.jpg"
width="1500px"
height="1000px">
document.addEventListener("DOMContentLoaded", ()=>{
let lazyImages = [].slice.call( document.getElementsByClassName('lazy') )
let active = false
const lazyLoad = function(){
if(active === true) return
active = true
setTimeout(()=>{
lazyImages.forEach(image => {
let rect = image.getBoundingClientRect()
if(rect.top <= window.innerHeight && rect.bottom >=0){
image.src = image.dataset.src
image.classList.remove('lazy')
lazyImages = lazyImages.filter((i)=>{
return i !== image
})
}
if(lazyImages.length === 0){
document.removeEventListener('scroll',lazyLoad)
window.removeEventListener('resize',lazyLoad)
}
});
active = false
},200)
}
document.addEventListener('scroll',lazyLoad)
window.addEventListener('resize',lazyLoad)
优点:兼容性较好,缺点:事件触发频繁,造成很多不必要的计算
Intersection Observer方式(交叉观察者)
思路:这种方式思路与传统方式相同,这是一种升级版,但区别在于监听事件的不同,传统方式的事件监听触发频率太高,很难优化,而Intersection Observer这个API翻译过来是交叉观察者,我们将延迟加载的图片作为被观察者,当图片与视口有交叉时,便会触发回调函数,在回调函数中修改src即可
document.addEventListener('DOMContentLoaded',function(){
let lazyImages = [].slice.call( document.getElementsByClassName('lazy') )
let lazyImageObserver = new IntersectionObserver(function(entries,observer){
entries.forEach((entry)=>{
if(entry.isIntersecting){
let image = entry.target
image.src = image.dataset.src
image.classList.remove('lazy')
lazyImageObserver.unobserve(image)
}
})
})
lazyImages.forEach(image => {
lazyImageObserver.observe(image)
});
})
优点:传统方式的升级版,简洁高效 缺点:某些浏览器可能不兼容Intersection Observer API
CSS类名方式
思路:与上述方式类似,但是用div标签替代img,用background-image来展示图片,浏览器会判断当div不可见时,就不会请求css资源,我们可以用js来判断图片是否在可视区域,当在可视区域在添加样式,请求资源。
原生的延迟加载支持
img和iframe标签上的loading属性支持了原生加载方式,loading属性有3个值
- lazy: 进行延迟加载
- eager: 立即执行加载
- auto: 浏览器自行决定是否延迟加载
<!-- 当用户滚动屏幕到图片时,才进行加载 -->
<img loading="lazy" src="http://e.hiphotos.baidu.com/image/pic/item/a1ec08fa513d2697e542494057fbb2fb4316d81e.jpg"
width="1500px" height="1000px">
<!-- 立即执行加载 -->
<img loading="eager" src="http://c.hiphotos.baidu.com/image/pic/item/30adcbef76094b36de8a2fe5a1cc7cd98d109d99.jpg"
width="1500px" height="1000px">
<!-- 浏览器自行决定是否延迟加载 -->
<img loading="auto" src="http://h.hiphotos.baidu.com/image/pic/item/7c1ed21b0ef41bd5f2c2a9e953da81cb39db3d1d.jpg"
width="1500px" height="1000px">
加载注意事项
首屏加载
首屏上的内容不应当进行延迟加载,因为DOMContentLoaded需要等待页面脚本加载执行完毕后才触发,首屏上的内容应立即加载。
资源占位
当延迟内容未加载完成时,应当在页面上使用尺寸相同的占位图像,如果不进行占位,当图片加载完成时,尺寸更改引起页面布局的变化,会造成昂贵的回流机制,所以我们可以用一个base64的图片进行占位。
资源加载失败
当图片资源未能按预期成功加载时,所采取的具体处理措施应当依据应用场景而定。比如,当请求的媒体资源无法加载时,可将使用的图像占位符替换为按钮,让用户单击以尝试重新加载所需的媒体资源,或者在占位符区域显示错误的提示信息。总之,在发生任何资源加载故障时,给予用户必要的通知提示,总好过直接让用户无奈地面对故障。
资源优先级
优先级
浏览器基于自身的启发式算法,会对资源的重要性进行判断来划分优先级,通常从低到高分为:Lowest、Low、High、Highest等。比如,在〈head〉标签中,CSS文件通常具有最高的优先级Highest,其次是〈script〉标签所请求的脚本文件,但当〈script〉标签带有defer或async的异步属性时,其优先级又会降为Low。
代码优化
- 作用域链不应过长
- 减少闭包的使用
- 减少全局变量
- if-else判断过多时应该换用switch
- 循环语句性能:三种常规循环 >
for of>forEachfor in - 递归优化:尾递归优化、递归改迭代
- 字符串拼接性能:
str = str + 'a' + 'b'性能优于str += 'a'+'b',原因在于第一种没有产生中间变量 - 减少dom重复操作
- 使用异步队列将大任务拆分成小任务
- 尽量使用原生方法
构建优化
压缩与合并
资源的压缩与合并的优点:减少HTTP的请求次数、减少HTTP的请求资源大小
HTML压缩
使用nodejs所提供的html-minifier工具进行压缩,它涉及很多参数的配置,包括是否去掉注释removeComments,是否去掉空格collapseWhitespace,是否压缩HTML中的JavaScript的minifyJS及是否压缩HTML中的CSS的minifyCSS
CSS压缩
可以使用html-minifier针对HTML中的CSS进行压缩。除此之外,还可以使用clean-css进行CSS的压缩
JavaScript压缩与代码混淆保护
js压缩可以减少代码的大小,还有另一个重点,经过压缩后的代码是很难阅读的,任何人都可以查看网页源代码,例如一个电商网站的前端支付代码未被压缩,有人会利用代码模拟出整个下单流程,进而找出可能的漏洞,所以压缩代码还能起到安全作用
文件合并
假如有a.js、b.js、c.js三个文件,那么就要发送三个HTTP请求,但如果将三个文件进行合并为一个文件,那么就只需要发送一个HTTP请求,进而节省时间。但合并也不是完美的,合并文件带来的第一个问题:合并后的文件太大,则会影响首页渲染,如果页面仅需要a.js文件才可以渲染,那么页面必须等到b.js和c.js都加载完毕后,才开始渲染,这无疑是耗时的。第二个问题:现在的网站都会用到缓存机制,给每个js文件打上一个ETag,如果合并文件只有一小部分发生了改变,也会缓存失效。
webpack的性能优化
渲染优化
JavaScript执行优化
requestAnimationFrame实现动画效果
实践经验告诉我们,使用定时器实现的动画会在一些低端机器上出现抖动或者卡顿的现象,这主要是因为浏览器无法确定定时器的回调函数的执行时机。以setInterval为例,其创建后回调任务会被放入异步队列,只有当主线程上的任务执行完成后,浏览器才会去检查队列中是否有等待需要执行的任务,如果有就从任务队列中取出执行,这样会使任务的实际执行时机比所设定的延迟时间要晚一些。
其次屏幕分辨率和尺寸也会影响刷新频率,不同设备的屏幕绘制频率可能会有所不同,而setInterval只能设置某个固定的时间间隔,这个间隔时间不一定与所有屏幕的刷新时间同步,那么导致动画出现随机丢帧也在所难免
为了避免这种动画实现方案中因丢帧而造成的卡顿现象,我们推荐使用window中的requestAnimationFrame方法。与setInterval方法相比,其最大的优势是将回调函数的执行时机交由系统来决定,即如果屏幕刷新频率是60Hz,则它的回调函数大约会每16.7ms执行一次,如果屏幕的刷新频率是75Hz,则它回调函数大约会每13.3ms执行一次,就是说requestAnimationFrame方法的执行时机会与系统的刷新频率同步。
这样就能保证回调函数在屏幕的每次刷新间隔中只被执行一次,从而避免因随机丢帧而造成的卡顿现象。
恰当使用Web Worker
众所周知JavaScript是单线程执行的,所有任务放在一个线程上执行,只有当前一个任务执行完才能处理后一个任务,不然后面的任务只能等待,这就限制了多核计算机充分发挥它的计算能力。同时在浏览器上,JavaScript的执行通常位于主线程,这恰好与样式计算、页面布局及绘制一起,如果JavaScript运行时间过长,必然就会导致其他工作任务的阻塞而造成丢帧。 为此可将一些纯计算的工作迁移到Web Worker上处理,它为JavaScript的执行提供了多线程环境,主线程通过创建出Worker子线程,可以分担一部分自己的任务执行压力。在Worker子线程上执行的任务不会干扰主线程,待其上的任务执行完
● DOM限制:Worker无法读取主线程所处理网页的DOM对象,也就无法使用document、window和parent等对象,只能访问navigator和location对象。
● 文件读取限制:Worker子线程无法访问本地文件系统,这就要求所加载的脚本来自网络。
● 通信限制:主线程和Worker子线程不在同一个上下文内,所以它们无法直接进行通信,只能通过消息来完成。
● 脚本执行限制:虽然Worker可以通过XMLHTTPRequest对象发起ajax请求,但不能使用alert()方法和confirm()方法在页面弹出提示。
● 同源限制:Worker子线程执行的代码文件需要与主线程的代码文件同源。
事件节流和事件防抖
计算样式优化
减少要计算样式的元素数量
首先我们需要知道与计算样式相关的一条重要机制:CSS引擎在查找样式表时,对每条规则的匹配顺序是从右向左的,这与我们通常从左向右的书写习惯相反。举个例子,如下CSS规则:
如果不知道样式规则查找顺序,则推测这个选择器规则应该不会太费力,首先类选择器.product-list的数量有限应该很快就能查找到,然后缩小范围再查找其下的li标签就顺理成章。
但CSS选择器的匹配规则实际上是从右向左的,这样再回看上面的规则匹配,其实开销相当高,因为CSS引擎需要首先遍历页面上的所有li标签元素,然后确认每个li标签有包含类名为product-list的父元素才是目标元素,所以为了提高页面的渲染性能,计算样式阶段应当尽量减少参与样式计算的元素数量
总结了如下几点实战建议:
使用类选择器替代标签选择器,对于上面li标签的错误示范,如果想对类名为product-list下的li标签添加样式规则,可直接为相应的li标签定义名为product-list_li的类选择器规则,这样做的好处是在计算样式时,减少了从整个页面中查找标签元素的范围,毕竟在CSS选择器中,标签选择器的区分度是最低的。
避免使用通配符做选择器,通常在编写CSS样式之前都会有使用通配符去清楚默认样式的习惯,如下所示:
这种操作在标签规模较小的demo项目中,几乎看不出有任何性能差异。但对实际的工程项目来说,使用通配符就意味着在计算样式时,浏览器需要去遍历页面中的每一个元素,这样的性能开销很大,应当避免使用。
降低选择器的复杂性
避免选择器的层级过多。
页面布局与重绘的优化
触发页面布局与重绘的操作
要想避免或减少页面布局与重绘的发生,首先就是需要知道有哪些操作能够触发浏览器的页面布局与重绘的操作,然后在开发过程中尽量去避免。
这些操作大致可以分为三类:
- 对DOM元素几何属性的修改,这些属性包括width、height、padding、margin、left、top等
- 对DOM树节点的增、删、移动等操作
- 获取某些特定的属性值操作,比如页面可见区域宽高offsetWidth、offsetHeight,页面视窗中元素与视窗边界的距离offsetTop、offsetLeft,类似的属性值还有scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientWidth、clientHeight及调用window.getComputedStyle方法。
这些属性和方法有一个共性,就是需要通过即时计算得到,所以浏览器就需要重新进行页面布局计算。
避免对样式的频繁改动
在通常情况下,页面的一帧内容被渲染到屏幕上会按照如下顺序依次进行,首先执行JavaScript代码,然后依次是样式计算、页面布局、绘制与合成。如果在JavaScript运行阶段涉及上述三类操作,浏览器就会强制提前页面布局的执行,为了尽量降低页面布局计算带来的性能损耗,我们应当避免使用JavaScript对样式进行频繁的修改。如果一定要修改样式,则可通过以下几种方式来降低触发重排或回流的频次。
- 使用类名对样式逐条修改
在JavaScript代码中逐行执行对元素样式的修改,是一种糟糕的编码方式,对未形成编码规范的前端初学者来说经常会出现这类的问题。错误代码示范如下:
上述代码对样式逐行修改,每行都会触发一次对渲染树的更改,于是会导致页面布局重新计算而带来巨大的性能开销。合理的做法是,将多行的样式修改合并到一个类名中,仅在JavaScript脚本中添加或更改类名即可。
- 缓存对敏感属性值的计算
有些场景我们想要通过多次计算来获得某个元素在页面中的布局位置,比如:
这不但在赋值环节会触发页面布局的重新计算,而且取值涉及即时敏感属性的获取,如offsetTop和offsetLeft,也会触发页面布局的重新计算。
作为优化我们可以将敏感属性通过变量的形式缓存起来,等计算完成后再统一进行赋值触发布局重排。