http://luckycola.com.cn/
前端性能优化一直是一个前端开发人员必须关注的经典话题,虽然现在随着技术的不断发展,网页容器(浏览器、webview)性能也越来越强大,但是网站应用的功能也不断丰富,体积不可避免的增加,当网络环境等因素不好时,仍然会存在白屏时间过长等严重影响用户体验的问题存在.因此,了解前端的性能优化是势在必行的工作,也是前端岗位面试中常见的问题.今天我们深入讨论本问题.
我们经常在面试中遇到面试官问这样一个问题:你知道浏览器渲染出一个页面的做了哪些事情?
其实这是前端考察中的一个非常经典的问题,也是我们今天讨论“如何做好前端性能优化”的核心背景问题.我们要知道:所有做好前端性能优化的方案都是要从问题根源出发的.所以,要做好前端的性能优化就必须了解其问题产生的原因是什么,那就需要简单理解下页面渲染的原理.
如果把浏览器呈现页面的整个过程一分为二,第一步主要是浏览器为呈现页面请求所需资源的部分;第二步是浏览器获取到资源后,进行渲染部分的相关优化内容。
第一部分主要取决于网络等环境因素, 第二部分取决“关键渲染路径(Critical Rendering Path)”的优化因素.也是我们需要着重讨论的优化方案部分:
关键渲染路径图
(1)、Dom tree
DOM 并非只能通过 JS 访问, 像可伸缩矢量图 SVG、数学标记语言 MathML和同步多媒体集成语言 SMIL都增加了该语言独有的 DOM
方法和接口。一旦HTML被解析,就会建立一个DOM树。下面的代码有三个区域:header
、main
和footer
。并且style.css
为外部文件。
当上述 HTML
代码被浏览器解析为 DOM树
状结构时,其各个节点的关系如下。
(2)、CSSOM Tree
CSSOM
也是一个基于对象的树。它负责处理与DOM树相关的样式。
对于上述CSS声明,CSSOM树
将显示如下。
由于,css
的部分属性能够被继承,所以,在父级节点定义的属性,如果满足情况,子节点也是会有对应的属性信息,最后将对应的样式信息,渲染到页面上。‘
(2)、Render Tree:
浏览器在构造DOM树的同时也在构造着另一棵树-Render Tree,它的主要作用就是把HTML按照一定的布局与样式显示出来,用到了CSS的相关知识。从MVC的角度来说,可以将render树看成是V,dom树看成是M,C则是具体的调度者,比HTMLDocumentParser等。
(一)、渲染关键路径优化
(1)、HTML文件的尺寸应该尽可能的小:
目的是为了让客户端尽可能早的接收到完整的HTML。通常HTML中有很多冗余的字符,
例如:JS注释、CSS注释、HTML注释、格、换行。更糟糕的情况是我见过很多生产环境中的HTML里面包含了很多废弃代码这可能是因为随着时间的推移,项目越来起大,由于种种原因从历史遗留下来的问题,不过不管怎么说,这都是很糟糕的。对于生产环境的HTML来说,应该剂除切无用的码,尽可能保证HTML文件精简。
(2)、优化CSSOM与阻塞渲染的CSS
CSS是构建渲染树的必备元素,首次构建网页时,JavaScript常常受阻于CSS。确保将任何非必需的CSS都标记为非关键资源(例如打印和其他媒体查询)(如图3.1),并应确保尽可能减少关键CSS的数量,以及尽可能缩短传送时间。
除了上面提到的优化策略,CSS还有一个可以影响性能的因素是:CSS会阻塞关键渲染路径。CSS是关键资源,它会阻塞关键渲染路径也并不奇怪,但通常并不是所有的CSS资源都那么的『关键』。
举个例子:一些响应式CSS只在屏幕宽度符合条件时才会生效,还有一些CSS只在打印页面时才生效。这些CSS在不符合条件时,是不会生效的,所以我们为什么要让浏览器等待我们并不需要的CSS资源呢?
如图3.1
(3)、避免在CSS中使用@import
大家应该都知道要避免使用@import加载CSS,实际工作中我们也不会这样去加载CSS,但这到底是为什么呢?
这是因为使用@import加载CSS会增加额外的关键路径长度。举个例子:
如图4.1当你在一个index.html文件中link一个style.css,然后在style.css中又import了一个main.css文件,此时两个依赖文件是串行加载的,加载完成样式文件是两个文件文件耗时的总和(图4.2)
图4.1:
图4.2:
(4)、优化加载js文件方式
浏览器在 html解析过程中,遇到scripe,会阻塞html页面渲染,发送页面请求,过去js脚本代码,然后交给js引擎去执行代码,当执行完毕后,浏览器会继续解析html。示意图如下:
此外还有preload、prefetch等常用方案
(二)、适当使用设计模式优化
(1)、使用单例模式防止重复实例化
很多时候一个页面我们只允许实例化一个功能固定的类,这样不仅利于代码的可读性和维护性,也可以很大程度保证性能的开销,减缓内存负载压力.那么怎么保证只允许实例化一个类呢?例子如下
(2)、使用享元模式进行大数据节点渲染(分页等场景)
在实际开发场景中,我们经常会遇到类似分页面的这种大数据前端渲染节点的需求,但是我们知道前端js直接操作行为是最消耗性能的行为,为了节省性能的开销,我们应该尽可能的减少对dom节点的直接操作.基于这个出发点,设计模式中的享元模式为我们提供了很好的解决方案.
接下来我们先看看不使用享元模式下的分页逻辑:
使用享元模式后的代码如下:
值得注意的是:
使用享元模式下的不管数据量多么庞大代码直接操作的dom节点只有那5个,分页行为并没有重复添加额外的dom节点,只是对相同的5个节点的内容进行了修改,5个节点始终是共享的.而未使用享元模式的则根据数据量进行同等数量的dom节点的创建和display属性的切换,两种方案的性能消耗显而易见.
(3)、适当使用节流(throttling)和防抖(debounce)
防抖与节流通常作为项目优化的手段,一般都是为了防止用户在短时间内快而频地多次操作,触发动作执行。比如防止用户点击多次提交按钮,触发表单多次提交;防止用户拉动滚动条,多次触发加载更多等情况.
debounce的特点是当事件快速连续不断触发时,动作只会执行一次。
防抖动和节流本质是不一样的。防抖动是多次触发但只会执行一次,节流是多次触发但周期内只会执行一次
throttling,节流的策略是,每个时间周期内,不论触发多少次事件,也只执行一次动作。上一个时间周期结束后,又有事件触发,开始新的时间周期,同样新的时间周期也只会执行一次动作。
除此之外设计模式多种多样,有兴趣可以阅读《javaScript设计模式》一书,这里只举几个经典的例子.
(三)、其他常见优化方案
(1)、懒加载
资源懒加载
加载的关键是 "懒加载"。任何媒体资源、CSS
、JavaScript
、图像、甚至HTML
都可以被懒加载。每次加载有限的页面的内容,可以提高关键渲染路径。
不要在加载页面时加载这个整个页面的 CSS
、JavaScript
和 HTML
。
相反,可以为一个button
添加一个事件监听,只有在用户点击按钮时才加载脚本。
使用Webpack
来完成懒加载功能。
这里有一些利用纯JavaScript实现懒加载的技术。
比如,现在又一个
在这些情况下,我们可以利用/
和标签附带的默认
loading
属性。当浏览器看到这个标签时,它会推迟加载iframe
和image
。
(2)、合理使用web worder
众所周知JavaScript是单线程执行的,所有任务放在一个线程上执行,只有当前一个任务执行完才能处理后一个任务,不然后面的任务只能等待,这就限制了多核计算机充分发挥它的计算能力。同时在浏览器上,JavaScript的执行通常位于主线程,这恰好与样式计算、页面布局及绘制一起,如果JavaScript运行时间过长,必然就会导致其他工作任务的阻塞而造成丢帧。
为此可将一些纯计算的工作迁移到Web Worker上处理,它为JavaScript的执行提供了多线程环境,主线程通过创建出Worker子线程,可以分担一部分自己的任务执行压力。在Worker子线程上执行的任务不会干扰主线程,待其上的任务执行完成后,会把结果返回给主线程,这样的好处是让主线程可以更专注地处理UI交互,保证页面的使用体验流程。
(3)、骨架屏优化白屏时长
使用骨架屏,可以缩短白屏时间,提升用户体验。国内大多数的主流网站都使用了骨架屏,特别是手机端的项目SPA 单页应用,无论 vue 还是 react,最初的 html 都是空白的,需要通过加载 JS 将内容挂载到根节点上,这套机制的副作用:会造成长时间的白屏常见的骨架屏插件就是基于这种原理,在项目打包时将骨架屏的内容直接放到 html 文件的根节点中使用骨架屏插件,打包后的 html 文件(根节点内部为骨架屏):
同一项目,对比使用骨架屏前后的 FP 白屏时间:
骨架屏插件推荐:vue-skeleton-webpack-plugin
结束语
前端性能优化一直是前端开发领域一个经典的话题,随着技术对发展也出现越来越多的解决方案,因为文章篇幅有限,这里只讨论了部分经典的优化方案,如果您有兴趣可以查阅更多资料和书籍进行分享.