不简单的前端性能优化

本文主要介绍“关键渲染路径”与“网络”两个方面的性能优化并提供demo,篇幅较长建议电脑观看。php

前端优化的方面太多,本文介绍的仅仅是其中的一部分,力求涵盖“关键渲染路径”的方方面面,及一些不常被提到的“网络优化”部分。css

测试环境如无特殊说明均为Chrome 57html


渲染页面过程

浏览器从打开一个URL到渲染完页面共有:前端

  • 下载HTML文档html5

  • 下载HTML文档中的csswebpack

  • 下载Js文件git

  • 执行js脚本github

  • 下载其余资源web

  • 经过HTML文档构建DOM(Parse HTML)浏览器

  • 经过CSS文件构建CSSOM(Parse CSS)

  • 经过DOM与CSSOM计算render tree

  • 根据render tree进行绘制,计算各个元素位置与大小(Layout)

  • 对页面进行上色,渲染为最终显示的像素(Paint)

第一次完成Paint称为“初次渲染”,这时候用户就能看到render tree里面的东西了。而完成初次渲染的过程称为“关键渲染路径”,关键渲染路径上须要加载的资源叫作“关键资源”

这个过程不少很复杂,其中的依赖关系也很复杂,笔者尝试画图来表示,可是实在是没画出来,因此仍是用文字来表述吧:

  • 引入的资源,哪怕被阻塞(好比被js脚本阻塞后续link标签),浏览器依旧会智能的预先加载它们(可是不执行)

  • “CSS文件的加载”会阻塞“Js文件执行”。若CSS引用在Js文件以前,“加载CSS文件”会阻塞“Js文件执行”。即CSS文件未加载解析完成前,js文件不会获得执行。由于js有可能会修改CSSOM。带有async和defer属性的script不受限制。

  • Parse HTML的解析是增量的,所以浏览器能够边下载HTML边构建DOM树

  • “CSS文件的加载”会阻塞“Layout”。若页面有正在加载的CSS文件,在CSS文件加载完以前,浏览器不会对页面进行Layout,这是为了防止样式突变带来的抖动

  • “加载Js文件”会阻塞“Parse HTML”,这个估计你们都知道了,由于js能够经过document.write修改HTML文档流

  • “Js文件执行”会几乎会阻塞全部东西,包括Layout

比较有意思的是,字体的加载会阻塞局部的渲染。若某一段文本的字体使用了一个还没有加载完的字体,这段文本则先不会被Paint,直到字体加载完或者超过某个时间(一般是3秒)文本才会忽然显示。

浏览器为了不FOUT(Flash Of Unstyled Text),会尽可能等待字体加载完成后,再显示应用了该字体的内容。只有当字体超过一段时间仍未加载成功时,浏览器才会降级使用系统字体。每一个浏览器都规定了本身的超时时间(Chrome是3秒)。但这也带来了FOIT(Flash Of Invisible Text)问题。内容没法尽快地被展现,致使空白

一些Demo来解释浏览器渲染流程

CSS会阻塞Layout:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css" />
    <!-- 这个css文件会加载3秒钟,在这个css加载完成前浏览器不会layout -->
    <link rel="stylesheet" href="../conn/sleep.php?sleep=3&content=h2{color:red;}" />
    <title>Title</title>
</head>
<body>
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

CSS会阻塞Js执行:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css" />
    <!-- 这个css文件会加载3秒钟 -->
    <link rel="stylesheet" href="../conn/sleep.php?sleep=3&content=h2{color:red;}" />
    <script>
        // 这段js会等待css加载完才会运行
        alert('js is run!');
    </script>
    <title>Title</title>
</head>
<body>
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

Js执行会阻塞关键渲染路径,哪怕是defer仍是async:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script>
        function sleep(ms){
            var ts =+new Date;
            while(true){
                if(+new Date -ts >=ms) break;
            }
            return +new Date -ts;
        }
    </script>
    <!-- 这个css文件会加载2秒钟,因此会在js文件以后加载完 -->
    <link rel="stylesheet" href="../conn/sleep.php?sleep=2&content=h2{color:red;}" />
    <!-- 这个js文件会瞬间加载完,可是会运行3秒钟 -->
    <script defer src="run3s.js"></script>
    <!-- 这个js文件会瞬间加载完,可是会运行2秒钟 -->
    <script async src="run2s.js"></script>
    <title>Title</title>
</head>
<body>
    <!-- 打开页面后5秒钟才会显示,由于js执行会阻塞关键渲染路径 -->
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

Foot会阻塞局部渲染,可是智能的浏览器会给他设定一个上限,通常是3秒钟:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        @font-face {
            font-family: "test-font";
            src: url("../conn/sleep.php?sleep=5&file=scripts_.ttf");
        }
        h1{
            font-family: "test-font";
        }
    </style>
    <title>Title</title>
</head>
<body>
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

CSS篇优化策略

优化核心概念是:将初次渲染不须要的CSS想办法剥离出关键渲染路径

若是仅仅是为了提早初次渲染时间而进行优化,将页面必备的CSS剥离关键渲染路径而形成样式突变致使页面抖动,则得不偿失了

使用link/style的media属性

对某些媒体查询条件触发后才使用的css,能够在link标签中加入media属性,以下:

<link rel="stylesheet" href="index_print.css" media="print">

此样式表仍会加载。当浏览器环境不匹配媒体查询条件时,该样式表不会阻塞渲染。咱们可针对不一样媒体环境拆分CSS文件,并为link标签添加媒体查询,避免为了加载非关键CSS资源,而阻塞初次渲染

使用DOM API添加CSS

可使用js代码来添加css

var style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'index.css';
document.head.appendChild(style);

使用resoure hint规范的preload

将link标签的rel属性设置为preload,浏览器遇到遇到标记为preload的link时,会开始加载它,可是因为rel不是stylesheet,所以不会阻塞渲染。

<link rel="preload" href="index_print.css" as="style" onload="this.rel='stylesheet'">

而后在适当的时候,在rel改成stylesheet,便可应用此样式。

可是这个属性兼容性比较差,详细能够参考这里。不过有一个polyfill能够用loadCSS,原理是经过DOM API插入样式资源。

这个属性的使用情景有些偏,也多是我理解问题:

当使用preload引入css文件时,实际上证实这个页面根本不须要这个css,它有多是打印样式,或者是响应式网站的另外一套css代码。可是,使用preload属性,浏览器反而会预先加载它,也就是说,在window.onload以前,用户将耗费了网络资源在加载一个暂时不须要的样式。网络资源不多是无限的,也就是说这个css会占用页面其余资源好比图片的网络资源。

询问瓜瓜老师本人后,瓜瓜老师说:

举个例子。第三屏有个广告版,它的样式

这样确实这个css的紧急程度就介于关键渲染路径的css与页面图片之间了,不过貌似这个情景很受限。

JS篇优化策略

使用defer延迟脚本执行

当script标签拥有defer属性时,该脚本会被推迟到整个HTML文档解析完后,再开始执行。所以将脚本放在head中,能够提前浏览器对脚本文件的加载,可是却不会阻塞parse HTML。

<script src="index.js" defer></script>
<!-- 百度统计代码 -->
<script src="tongji.js" defer></script>

注意,defer的脚本不会被css阻塞,parse HTML完成后当即执行,可是有可能会阻塞关键渲染路径。为何说有可能呢,假如脚本文件在render tree生成前加载完毕,则会开始执行,执行过程当中会阻塞关键渲染路径。请参考这个Demo

被defer的脚本,在执行时会严格按照在HTML文档中出现的顺序执行,可是实际上貌似不是这样,js文件先后文件如有依赖需慎重使用。

使用async延迟脚本执行

和defer相似,只是当js加载完后立刻执行,而不在意parse HTML是否完成,所以假如脚本比css先加载完,也会阻塞关键渲染路径。

<script src="index.js" defer></script>
<!-- 百度统计代码 -->
<script src="tongji.js" defer></script>

使用DOM API

据笔者所知,这是惟一一种100%不会阻塞关键渲染路径的js脚本加载方式。经过DOM API引入的js脚本会等到页面Layout和Paint后再开始执行,不论你将载入js文件的代码放在head中仍是body后面亦是如此。

其余的优化

使用Web Font Loader加载字体

若不想让字体阻塞局部渲染,可以使用Web Font Loader

网络优化篇

网络优化和CSS优化策略相同,尽量让关键资源提早加载完,因此优化时尽可能将如下指标压缩到最低:

  • 关键资源数

  • 关键资源体积

  • 关键资源网络来回数

固然,若是你的项目使用了先进的SPDY或HTTP/2,下面的方法可能并不适用。

优化关键资源数

RFC2616规定同域名同时只能有 2 个链接(RFC7230 中无限制),而​​​​​​​现代浏览器通常容许同域6个并发链接。所以,当页面中有许多须要外链的资源(script、link等),浏览器最多在每一个域同时并发下载6个。

每个请求,若使用域名,则须要额外增长一次DNS查询时间(若缓存未过时会命中缓存),所以一个网站过多的使用不一样域名的资源会额外增长DNS查询开销,这点在移动端很是明显。

固然,每一个请求创建根据TCP协议规定,还须要先进行3次捂手才能够创建连接。

合并请求

尽量的合并请求,减小网络请求数。这一点可能在其余性能优化文章都说烂了:

  • 小图片转base64

  • 合并打包CSS、JS文件

如今的比较流行的webpack就很是擅长作这种事情

适度使用内联CSS和Js

使用内联的CSS和JS当然能够减小请求,可是使用内联也意味着你的CSS和JS将不会再被浏览器缓存,所以要适度的使用内联,内联不是万能的。

从HTTP协议下手

最佳方案确定是过渡到HTTP/2无疑,可是如今HTTP/2的支持并不算太好,并且各大浏览器仅支持TLS下实现的HTTP/2(说白了就是HTTPS),使得HTTP/2的使用存在许些限制。

若是没有HTTP/2,或许能够:

  • 使用Keep-Alive能够规避TCP三次握手的时间

  • 使用Transfer-Encoding:chunked分块输出文件,还记得parse HTML的过程是增量的吗?若浏览器能够边下载HTML文件边解析,岂不美哉?

  • 减小重定向,这个看上去理所固然可是实际上却很容易被忽略

适度使用域名散列

浏览器同域并行下载数量有限,因此只要多创建几个二级域名就行了,而后合理的分配各个资源就行了。

假如因为某些不可抗拒缘由,关键资源数是12个,那么只要创建2个二级域名分别分配给其中的12个资源,浏览器会同时并行下载它们了。

不过,使用域名散列要适度,每个域名都须要额外的增长一次DNS查询时间。固然,DNS自己也有缓存,或许适当的增长DNS TTL时间也是个不错的主意。

压缩关键资源体积

对于js、css文件,如今网上现成的压缩工具一堆,并且应用十分普遍,相信你们都知道了,这里就很少说了。

说到压缩,服务器开启必定的压缩策略(如gzip)是个不错的主意,效果拔群,资源大概会压缩到原有的1/3左右。

图片压缩,这个须要知道什么情境下适合什么类型的图片,GIF、JPG、PNG使用情景各不相同,具体能够参考这篇文章:图片格式那么多,哪一种更适合你?

关键资源网络来回数

假如一个页面须要引入2个CSS才能工做,下面有2种方式

  • 2个均用link引入

  • 1个用link引入,在css中import另外一个css

毫无疑问确定是前者快,由于前者的网络来回数是1,然后者是2。

所以,尽量将资源加载扁平化,减小关键资源网络来回数是个不错的主意。

固然,优化时要注意的点也有很多,好比前面提到的浏览器同域并发限制等,须要权衡使其不要影响到其余的致使初次渲染时间延后。

一些无效的优化策略

使用document.write打印link标签引入css仍会阻塞初次渲染。

引用

奇舞团@瓜瓜老师:

奇舞团@屈屈老师:

W3C规范:

相关文章
相关标签/搜索