web的基本性能优化

本文通过介绍从输入网址到网页加载完成的过程中,浏览器的各类行为,分析各个步骤的关键因素来优化网页的性能
在了解web性能优化前,我们需要知道一些基本的概念:

前端性能优化有一个指标,叫做“关键路径渲染优化”,指的是从DOM树构建到绘制到屏幕上的整个过程中的优化因素

关于首屏和白屏时间:

  • 白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。
  • 首屏时间是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。
  • 白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素
  • 首屏时间 = 地址栏输入网址后回车 - 浏览器第一屏渲染完成 -影响白屏时间的因素:网络,服务端性能,前端页面结构设计。
  • 影响首屏时间的因素:白屏时间,资源下载执行时间。

参考

DOMContentLoaded是指页面元素加载完毕($(document).ready就是监听该方法),此时的一些静态资源如图片还不可见,但是页面是可以交互的;load是值所有资源都加载完毕($(document).load)监听的就是该事件

浏览器的结构

1.用户界面

2.浏览器引擎

在用户界面和渲染引擎之间传输指令

3.渲染引擎

负责显示请求的内容,解析获取到的资源将其呈现在屏幕上。chrome用的是Blink,firefox用的是Gecko。。。

4.网络

5.用户界面后端

绘制浏览器的基本组件,如窗口,由系统调用

6.JavaScript解释器

解析和执行js

7.数据存储

在硬盘上存储各类数据,如cookie、localstorage

基本的工作流程

1.加载内容

当我们在浏览器地址栏中输入网址后,浏览器会提交URL,DNS服务器进行域名解析后,再向对应的服务器发起请求,接收文件(包括了html、css、js、静态资源等等)

2.解析资源

浏览器在收到服务端发来的资源后,会进行解析,建立对应的结构树(如html的DOM树,CSSOM树)

HTML的解析

一般的,浏览器引擎从网络获取到文档后,会将文档分成8kb大小的分分块进行传输。
HTML解析器对HTML文档进行解析,生成解析树(以DOM元素以及属性作为节点的树)
浏览器对于html的解析具有一定的容错机制,会纠正错误无效的内容,尽管如此我们也应该避免。
解析树构建完成后,浏览器开始加载网页的外部资源,首先进行css样式文件的解析,每个css文件都会被解析成一个样式表对象。
css解析规则 css的样式匹配是从左往右进行的,如果换成从右往左,那么就要找到最顶层的元素再依次向下寻找,如果都不匹配就回到最顶层元素,通过另外一条路径进行匹配,这样就大大增加了成本。所以在书写css的时候应该尽可能高效:

  • 不要在ID选择器前使用标签名
  • 尽量不要在class选择器前使用标签名
  • 尽量减少嵌套层级
  • 避免使用通配符
  • 避免使用后代选择符,尽量使用子选择符,子元素匹配符的概率要大于后代元素匹配符
  • 减少内联css

3.渲染

将前面解析到的DOM和CSSOM合并成为一个渲染树(render),构建渲染树时,对每个元素进行位置、样式的计算,生成布局(Layout),然后将内容绘制在屏幕上(Paint)

阻塞解析和阻塞渲染的因素

渲染树是有DOM树和CSSOM树构建而成,所以,DOM和CSSOM的构建会影响到后续的渲染。其中,js的解析会阻塞DOM树的渲染(因为js的操作可能引起回流,且浏览器无法预估它会有什么具体的操作,并且js是加载后马上执行的),而css不会阻塞DOM的解析但会阻塞DOM的渲染(前提是没有js对DOM和样式进行处理)。
并且,CSSOM的构建会影响到js的执行,如果在浏览器未完成CSSOM的下载和构建时,执行js,那么浏览器会延迟js的执行和DOM的构建,直到完成CSSOM的下载和构建。我们可以理解为,CSSOM构建完成时间是早于js的执行的,二者是早于DOMContentLoaded(DOM构建彻底完成)的。
正因为如此,我们也可以很好理解,为什么<script>标签通常是放在html文档下面,而<link>放在顶部。
为了防止js阻塞DOM的解析,我们应该尽可能合并脚本,并且将script放在页面body闭合标签前,也可以在<script>标签内增加deferasync属性,或者通过js动态插入脚本。

deferasync的区别:

  • async在加载和渲染后续文档的过程中将和js的加载与执行并行进行
  • defer在加载后续文档的过程中将和js的解析并行进行,但是js的执行要在所有的元素解析完成后,DOMContentLoaded事件触发之前完成。使用defer的js执行不一定按照顺序进行,谁先加载完成就先执行,所以显得不是很严谨

用js来动态加载:

function loadScript(url, callback){
    var script = document.createElement('script');
    script.type = 'text/javascript';
    if(script.readyState){  //IE下
        script.onreadystatechange = function(){
            if(script.readyState == 'loaded' || script.readyState == 'complete'){
                script.onreadystatechange = null;
                callback();
            }
        }
    }else{ //其他浏览器
        script.onload = function(){
            callback();
        }
    }
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}

CSSOM构建的优化:
在渲染树的关键渲染路径中,CSSOM也扮演着很重要的角色,我们应该想办法尽可能精简它,除此,我们还可以使用媒体类型和媒体查询来接触对渲染的阻塞:

<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">
  • 第一个资源会加载并阻塞。
  • 第二个资源设置了媒体类型,会加载但不会阻塞,print 声明只在打印网页时使用。
  • 第三个资源提供了媒体查询,会在符合条件时阻塞渲染。

至于一些图片之类的静态资源,实际上,它们并不会阻塞首屏的渲染(所以有些网页的结构已经加载完成了,但是图片还没加载完),但是我们也应该考虑用户的体验优化图片的加载,雪碧图和懒加载是我们常用的一些方法。

简单概括一下阻塞解析和阻塞渲染的因素:
javascript阻止DOM构建(DOMCommentLoaded触发被延迟),css的下载和完成阻止javascript的执行。在没有javascript或者只含有异步javascript的页面中,DOM的构建和CSSOM的构建互不影响。

网页首次加载完成后的性能优化

网页加载完成后,我们还是可能会对DOM进行操作,这个过程同样会涉及到网页的性能优化。

重绘(Reflow)和回流(Repaint)

它们的关系是:回流会导致重绘,重绘不一定会回流

  • 重绘:当页面中元素样式的改变不影响它在文档流中的位置时,比如颜色、背景色、透明度等,浏览器会根据样式的变化重新对变化的元素进行绘制

  • 回流:当文档中的元素尺寸、结构等属性发生变化时,浏览器会重新渲染部分或全部的文档的过程 会触发回流的的操作:

  • 页面首次渲染

  • 浏览器窗口尺寸改变

  • 元素尺寸或位置改变(position为absolute和fixed时的位置变化不触发)

  • 元素内容变化

  • 添加或删除可见的DOM元素

  • 激活css伪类

  • 通过js获取某些属性或方法,比如clientWidth、getComputedStyle、getBoundingClientRect......

如何避免回流:
CSS的操作:

  • 避免使用<table>
  • 尽可能在DOM树末端改变class
  • 避免设置多层内联样式
  • 将动画应用到position为absolute或fixed的元素上
  • 避免使用css表达式如calc

js的操作:

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

其他的性能优化

关于动画

最普通的动画,我们可能会使用setTimeout来实现,但是由于不同显示器的刷新率可能不一样,可能会导致动画掉帧的现象,所以,我们可以用css3动画或者requestAnimationframe来优化

关于DOM的监听

在js中,我们可能会对DOM做长时间的监听,如果事件触发很频繁,但是对用户体验却毫无意义,我们可以使用防抖和节流来优化

实验性的功能

preload

preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。它给我们带来的好处:

  • 将加载和执行分离,防止阻塞渲染
  • 优化用户体验,不会出现网页结构加载完成而部分静态资源还没加载出来

哪些类型的内容可以被预加载?
参考:MDN

怎么用?
一个基本的例子:

  <link rel="preload" href="style.css" as="style">
  <link rel="preload" href="main.js" as="script">

注意,要使用 as 来指定将要预加载的内容的类型
如果涉及到跨域获取资源,我们可以设置crossorigin属性:

<link rel="preload" href="fonts/cicle_fina-webfont.svg" as="font" type="image/svg+xml" crossorigin="anonymous">

如果是需要响应式开发,可以使用媒体查询,像下面这样:

  <link rel="preload" href="bg-image-narrow.png" as="image" media="(max-width: 600px)">
  <link rel="preload" href="bg-image-wide.png" as="image" media="(min-width: 601px)">

预加载的资源的优先级全部会提高,所以一些不确定的资源也是会被预先加载的,如果盲目使用预加载,势必增加移动端用户的流量
预加载机制的兼容性较差,目前IE都无法兼容,所以谨慎使用

prefetch

prefetch允许浏览器在后台(空闲时)获取将来可能用得到的资源,并且将他们存储在浏览器的缓存中
使用类似preload:<link rel="prefetch" href="/uploads/images/pic.png">
preload 和 prefetch 混用的话,并不会复用资源,而是会重复加载。

移动端的一些性能优化

  • 不要给非static的定位元素增加css动画,比如relative、absolute
  • 尽可能设置硬件加速,比如在渲染图片时可以使用canvas绘图。或者给元素设置transform: translate3d(0,0,0),这样元素本身没有外观上的变化,但是它的渲染会触发GPU加速



参考