# 5-1 前端性能优化

# 介绍

任何小型企业,博主或成长型公司在网上出现时面临的最大挑战之一就是 “页面加载时间” 问题。除非您能确保尽可能快速无缝地加载每个页面,您才可以拥有世界上最好最引人注目的网站,否则您仍然无法赢得关注者的尊重和忠诚度。

根据当今网络上的所有主要浏览器公司的说法,如果每个网站都想要排名靠前,表现良好并确保客户满意,那么每个网站都需要快。毕竟,如果您的网页加载速度很慢,那么您会立即增加客户开始在其他地方搜索信息,而不是坚持下去的风险。

这也是为什么很多公司去找专门的技术人员来做 SEO(Search Engine Optimization),即搜索引擎优化。 因为,页面的加载会影响到爬虫的爬取,页面加载速度是一项非常重要的指标。况且,页面优化直接影响到了用户体验。所以学习前端性能优化势在必行。

经常有面试官会问:从用户输入浏览器输入 url 到页面最后呈现 有哪些过程?有什么办法能优化页面性能?

答案大致如下:

  1. 用户输入 URL 地址
  2. 浏览器解析 URL 解析出主机名
  3. 浏览器将主机名转换成服务器 ip 地址(浏览器先查找本地 DNS 缓存列表 没有的话 再向浏览器默认的 DNS 服务器发送查询请求 同时缓存)
  4. 浏览器将端口号从 URL 中解析出来
  5. 浏览器建立一条与目标 Web 服务器的 TCP 连接(三次握手)
  6. 浏览器向服务器发送一条 HTTP 请求报文
  7. 服务器向浏览器返回一条 HTTP 响应报文
  8. 关闭连接 浏览器解析文档
  9. 如果文档中有资源 重复 6 7 8 动作 直至资源全部加载完毕

/images/image-20190619100048897

再深一点层次:会问到渲染机制,重排、重绘

重排:

  1. 定义:DOM 结构中的各个元素都有自己的盒子 (模型),这些都需要浏览器根据各种样式来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为 reflow。

  2. 触发 Reflow:

    • 当你增加、删除、修改 DOM 结点时,会导致 Reflow 或 Repaint
    • 当你移动 DOM 的位置,或是搞个动画的时候
    • 当你修改 CSS 样式的时候
    • 当你 Resize 窗口的时候 (移动端没有这个问题),或者是滚动的时候
    • 当你修改网页的

    最常问:如何减少 Reflow? 或者避免 Reflow?

重绘:

  1. 定义:当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称之为 repaint。

    页面要呈现的内容,通通都绘制到页面上。

  2. 触发 Repaint

    • DOM 改动
    • CSS 改动

    最常问:如何避免最小程序的 Repaint?

既然页面的性能优化这么重要,** 你知道有哪些原因是影响页面性能的?** 比如...

  • HTTP 请求
  • 复杂的页面逻辑 (JS 设计)
  • 重度的 DOM 操作
  • 服务端响应
  • 大量的数据

...

有哪些办法可以加速我们的页面性能?比如...

  • 资源压缩与合并(代码打包)
  • 异步加载
  • CDN
  • DNS 预解析
  • 缓存

...

主要内容:

  • 页面加载缓慢原因分析
  • 性能优化原则
  • 优化工具介绍
  • 浏览器的工作机制,了解一些基本常识的原理:
    1. 为什么要将 js 放到页脚部分
    2. 引入样式的几种方式的权重
    3. css 属性书写顺序建议
    4. 何种类型的 DOM 操作是耗费性能的

收获

  • 前端页面优化,为成为 SEO 路添砖加瓦
  • 写出优质代码,页面性能蹭蹭蹭的往上走
  • 面试如果提到页面优化,可以娓娓道来,牛皮吹的飞起...
  • 几种常见的页面优化工具的使用 PageSpeed,JSPerf

准备:

  • 注册 github 账号

# 页面性能优化

前端性能优化可以分为两大部分:浏览器部分、代码部分。

浏览器部分又可以分为:

  • 网络层面
  • 浏览器渲染层面
  • 服务端层面

代码部分又可以分为:

  • 构建层面
  • 编码层面
  • 机制 (SSR,英文 Server Side Render:服务器端渲染)
  • 规范

首先来看看浏览器部分:前端的页面主要在浏览器上运行着,那么我们追根溯源,从浏览器的原理开始,研究如何进行页面性能优化。

# 浏览器

目前使用的主流浏览器有五个:Internet Explorer、Firefox、Safari、Chrome 浏览器和 Opera。本文中以开放源代码浏览器为例,即 Firefox、Chrome 浏览器和 Safari(部分开源)。根据 StatCounter 浏览器统计数据,目前(2019 年 6 月)Firefox、Safari 和 Chrome 浏览器的总市场占有率 62.7%。由此可见,如今开放源代码浏览器在浏览器市场中占据了非常坚实的部分。

/images/image-20190618165435861

# 浏览器的主要作用

浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展示您选择的网络资源。这些网络资源包括以下内容:

  • HTML
  • CSS
  • JavaScript 的
  • 媒体(图片,视频等)

也可以分为 HTML 文档 (HTML/CSS/JS)、PDF、图片、视频和其他类型。

浏览器解释并显示 HTML 文件的方式是在 HTML 和 CSS 规范中指定的。这些规范由网络标准化组织 W3C(万维网联盟)进行维护。
多年以来,各浏览器都没有完全遵从这些规范,同时还在开发自己独有的扩展程序,这给网络开发人员带来了严重的兼容性问题。如今,大多数的浏览器都是或多或少地遵从规范。

浏览器的用户界面有很多彼此相同的元素,其中包括:

  • 用来输入 URI 的地址栏
  • 前进和后退按钮
  • 书签设置选项
  • 用于刷新和停止加载当前文档的刷新和停止按钮
  • 用于返回主页的主页按钮

奇怪的是,浏览器的用户界面并没有任何正式的规范,这是多年来的最佳实践自然发展以及彼此之间相互模仿的结果。HTML5 也没有定义浏览器必须具有的用户界面元素,但列出了一些通用的元素,例如地址栏、状态栏和工具栏等。

有一些比较有趣的事情:

Safari 是乔布斯在 2003 年的 Macworld 大会发布的,9 年过去了,08 年才发展起来的 Chrome 用了不到 Safari 一半的时间远远把 Safari 甩在了后面,这让 Safari 情何以堪。

两大浏览器都是基于苹果的布局引擎 Webkit,按道理 Safari 是占优势的,因为两者都分别是自己操作系统的默认浏览器,许多人就顺便使用了,懒得去下载别的浏览器。

许多人可能会想,这还不简单,Chrome 运行于 Windows 操作系统,而 Safari 大部分时候都用于苹果自己的系统,Windows .VS. OS X,哪个使用人群广?很显然是前者,所以 Chrome 当然比 Safari 发真快,可是,实际上 Safari 要比 Chrome 更早登陆 Windows。

在 2007 年六月,Safari for Windows Beta 版就已经发布,正式版于 2008 年三月发布,而 Chrome 直到 2008 年的九月才发布,Chrome 只用了一年的时间就超过了 Safari。

大量 Benchmark 测试表明 Chrome 不管在网页打开还是 JavaScript 表现方面,速度都最快。

但是,当 Safari 发布 Windows 版的时候,测试结果也是一样的,不管在 Mac 还是 PC 上测试,那时候都是最快的,如果仅仅是因为速度,那么 Safari 在 2007 年六月的表现就应该和 chrome 在 2008 年九月的时候一样。

难道 Chrome 胜在插件?也许,不过 Safari 在 2010 年年中的时候也有了许多插件,当然,Chrome 的插件质量更好更丰富,如果这也足够让 Safari 落后于 Chrome,那么开发商们可能早就将游戏植入 Chrome,而且,Firefox 也先于 Chrome 和 Safari 植入插件,现在不也败给了 Chrome 吗?

主要原因:

  1. Safari 用起来不舒服,google 推广给力
  2. 独特的用户体验 (UI、交互、云、Google 全家桶)

那么 Safari 就一文不值了?

但是,Mac 上的用户使用 safari 更加的省电、省资源,稳定性会更好。** 插件不需要爬梯子!!!** 更低的学习成本。

IE (Internet Explorer) 的黑历史:

IE 浏览器的开发应该是在 1994 年中开始的,那是我还在读大学。微软为了对抗当时的主流浏览器 NetScape,准备开发自己的浏览器并在 Windows 中默认捆绑销售。但是,留给微软的时间并不充裕,他们没时间从零开始,于是和 Spyglass 合作开发,于是,计算机历史上最著名也最臭名昭著的浏览器 IE 诞生了。因为强大的功能和捆绑策略,IE 迅速击败了 NetScape,成为浏览器市场的绝对霸主。

在 2002 年,IE 已经拥有了 95% 的市场份额,几乎打败了所有的竞争对手。大为僵,又不思进取,于是颠覆者前仆后继。Firefox 开始迅速崛起,Chrome 后来居上,反观 IE,出现了各种安全漏洞和兼容性问题。2006 年,IE 被评为 “史上第八糟科技产品”。在相当长的时间里,IE 浏览器是前端程序员员的噩梦

带给我们的思考:

  • 软件迭代更新的重要性 (居安思危)
  • 用户体验、用户感知的重要性 (人性化产品)
  • 互联网时代,什么都是可以替代的
  • 分享、免费、共筑共赢才是互联网的精神

# 浏览器的组成结构

img

  1. 用户界面(User Interface) - 包括地址栏、前进 / 后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。

  2. 浏览器引擎(Browser engine) - 在用户界面和渲染引擎之间传送指令。

  3. 渲染引擎(Rendering engine) - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。

    也可以叫呈现引擎(Rendering Engine)或者布局引擎(Layout Engine)

    默认情况下,渲染引擎可显示 HTML 和 XML 文档与图片。通过插件(或浏览器扩展程序),还可以显示其他类型的内容;例如,使用 PDF 查看器插件就能显示 PDF 文档。但是在本章中,我们将集中介绍其主要用途:显示使用 CSS 格式化的 HTML 内容和图片。

    浏览器(Firefox、Chrome 浏览器和 Safari)是基于两种渲染引擎构建的。Firefox 使用的是 Gecko,这是 Mozilla 公司 “自制” 的渲染引擎。而 Safari 和 Chrome 浏览器使用的都是 WebKit。

    浏览器渲染引擎(开发语言)脚本引擎(开发语言)
    ChromeBlink (c++)V8 (c++)
    OperaBlink (c++)V8 (c++)
    SafariWebkit (c++)JavaScript Core (nitro)
    FireFoxGecko (c++)SpiderMonkey (c/c++)
    EdgeEdgeHTML (c++)Chakra JavaScript Engine (c++)
    IETrident (c++)Chakra JScript Engine (c++)

    Firefox 在 2017 年启用了新的 web 引擎 Quantum,Quantum 以 Gecko 引擎为基础,同时利用了 Rust 的良好并发性和 Servo 的高性能组件,为 Firefox 带来了更多的并行化和 GPU 运算,让 Firefox 更快更可靠。

    2015 年 3 月,微软将放弃自家 Edge:转而开发 Chromium 内核浏览器。

    它们的开发时间轴:

    /images/image-20190618172835156

    WebKit 是一种开放源代码渲染引擎,起初用于 Linux 平台,随后由 Apple 公司进行修改,从而支持苹果机和 Windows。有关详情,请参阅 webkit.org

  4. 网络(Networking) - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。

  5. 用户界面后端(UI Backend) - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。

  6. JavaScript 解释器(JavaScript Interpreter)。用于解析和执行 JavaScript 代码,如 V8 引擎。

    JS 引擎线程负责解析 Javascript 脚本,运行代码。

    JS 引擎一直等待任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中只有一个 JS 线程在运行

  7. 数据存储(Data Persistence)。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了 “网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。

# 浏览器是多进程的

进程线程简单的理解:进程里面可以有多个线程,进程就是 QQ,线程就是会话。

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
  • 简单点理解,每打开一个 Tab 页,就相当于创建了一个独立的浏览器进程。

浏览器里面的进程:

  1. Browser 进程:浏览器的主进程(负责协调、主控),只有一个。作用有

    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将 Renderer 进程得到的内存中的 Bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  2. 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建

  3. GPU 进程:最多一个,用于 3D 绘制等

  4. 渲染进程(浏览器内核)(Renderer 进程,内部是多线程的)

    • 默认每个 Tab 页面一个进程,互不影响。
    • 主要作用为页面渲染,脚本执行,事件处理等

渲染进程是多线程的:

  1. GUI 渲染线程

    • 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流 (reflow) 时,该线程就会执行
    • 注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
  2. js 引擎线程

    • 也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)
    • JS 引擎线程负责解析 Javascript 脚本,运行代码。
    • JS 引擎一直等待任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中只有一个 JS 线程在运行
    • 同样注意,GUI 渲染线程与 JS 引擎线程是互斥的。所以如果 JS 执行的时间过长,要放在 body 下面,否则就会导致页面渲染加载阻塞。
  3. 事件触发线程

    • 管理着事件队列
    • 监听事件,符合条件时把回调函数放入事件队列中
  4. 定时触发器线程

    • setInterval 与 setTimeout 在此线程中计时完毕后,把回调函数放入事件队列中
    • 浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确),因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行)
    • 注意,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms
  5. 异步 http 请求线程

    • 检测到 XHR 对象状态变化时,将回调函数放入事件队列中
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。

    了解 一下,执行线:

    /images/image-20190619165952240

渲染线程与 JS 引擎线程互斥

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JS 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JS 引擎为互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起, GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行。

举一个简单的例子:

<!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>Document</title>
    <script>
      // 页面上的 hello world 不会变成红色
      document.getElementById("app").style.color = "red";
    </script>
  </head>
  <body>
    <div id="app">hello world!</div>
  </body>
</html>

JS 阻塞页面加载

从上述的互斥关系,可以推导出,JS 如果执行时间过长就会阻塞页面。

譬如,假设 JS 引擎正在进行巨量的计算,此时就算 GUI 有更新,也会被保存到队列中,等待 JS 引擎空闲后执行。 然后,由于巨量计算,所以 JS 引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。

所以,要尽量避免 JS 执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

总结一下:

  • css 加载不会阻塞 DOM 树解析(异步加载时 DOM 照常构建),但会阻塞 render 树渲染(渲染时需等 css 加载完毕,因为 render 树需要 css 信息)
  • Javascript 阻塞 DOM 解析

# 浏览器的渲染机制

对于渲染,我们首先需要了解一个概念:设备刷新率。

设备刷新率是设备屏幕渲染的频率,通俗一点就是,把屏幕当作墙,设备刷新率就是多久重新粉刷一次墙面。基本我们平常接触的设备,如手机、电脑,它们的默认刷新频率都是 60FPS,也就是屏幕在 1s 内渲染 60 次,约 16.7ms 渲染一次屏幕。

这就意味着,我们的浏览器最佳的渲染性能就是所有的操作在一帧 16.7ms 内完成,能否做到一帧内完成直接决定着渲染性,影响用户交互。

渲染引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内。

然后进行如下所示的基本流程

img

渲染引擎将开始解析 HTML 文档,并将各标记逐个转化成 “内容树” 上的 DOM 节点。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于创建另一个树结构:呈现树

呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。

呈现树构建完毕之后,进入 “布局” 处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。下一个阶段是绘制 - 渲染引擎会遍历呈现树,由用户界面后端层将每个节点绘制出来。

需要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,渲染引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,渲染引擎会将部分内容解析并显示出来。

那么我们知道了,CSSOM 树和 DOM 树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。优化上述每一个步骤对实现最佳渲染性能至关重要。

具体的流程:

  • DOM 树与 CSSOM 树合并后形成渲染树。
  • 渲染树只包含渲染网页所需的节点。
  • 布局计算每个对象的精确位置和大小。
  • 最后一步是绘制,使用最终渲染树将像素渲染到屏幕上。

DOM CSSOM RenderTree

请注意 visibility: hiddendisplay: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 ( display: none ) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。

我们以 webkit 的渲染作为示例:

img

我们可以使用调试工具来看一个真实的例子:

/images/image-20190618182337662

script 标签的处理

JS 可以操作 DOM 来修改 DOM 结构,可以操作 CSSOM 来修改节点样式,这就导致了浏览器在解析 HTML 时,一旦碰到 script ,就会立即停止 HTML 的解析(而 CSS 不会),执行 JS,再返还控制权。

事实上,JS 执行前不仅仅是停止了 HTML 的解析,它还必须等待 CSS 的解析完成。当浏览器碰到 script 元素时,发现该元素前面的 CSS 还未解析完,就会等待 CSS 解析完成,再去执行 JS。

JS 阻塞了 HTML 的解析,也阻塞了其后的 CSS 解析,整个解析进程必须等待 JS 的执行完成才能够继续,这就是所谓的 JS 阻塞页面。一个 script 标签,推迟了 DOM 的生成、CSSOM 的生成以及之后的所有渲染过程,从性能角度上讲,将 script 放在页面底部,也就合情合理了。

简单来说:渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时渲染线程会被挂起(相当于被冻结了),渲染更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。

总结一下:

浏览器的渲染流程分为:

  • DOM 树构建
  • CSSOM 树构建
  • RenderObject 树构建
  • 布局
  • 绘制

# 重排 reflow 与重绘 repaint

重排 reflow

reflow 指的是重新计算页面布局。

某个节点 reflow 时会重新计算节点的尺寸和位置,而且还有可能触发其子节点、祖先节点和页面上的其他节点 reflow。在这之后再触发一次 repaint。

当 render tree 中的一部分 (或全部) 因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流,每个页面至少需要一次回流,就是在页面第一次加载的时候。

导致 reflow 的操作

  • 调整窗口大小
  • 改变字体
  • 增加或者移除样式表
  • 内容变化,比如用户在 input 框中输入文字
  • 激活 CSS 伪类,比如 :hover (IE 中为兄弟结点伪类的激活)
  • 操作 class 属性
  • 脚本操作 DOM
  • 计算 offsetWidth 和 offsetHeight 属性
  • 设置 style 属性的值

触发页面重布局的一些 css 属性

  • 盒子模型相关属性会触发重布局
    • width
    • height
    • padding
    • margin
    • display
    • border-width
    • border
    • min-height
  • 定位属性及浮动也会触发重布局
    • top
    • bottom
    • left
    • right
    • position
    • float
    • clear
  • 改变节点内部文字结构也会触发重布局
    • text-align
    • overflow-y
    • font-weight
    • overflow
    • font-family
    • line-height
    • vertical-align
    • white-space
    • font-size

重绘 repaint

repiant 或者 redraw 遍历所有的节点检测各节点的可见性、颜色、轮廓等可见的样式属性,然后根据检测的结果更新页面的响应部分。

当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color。则就叫称为重绘

只触发重绘不触发重排的一些 CSS 属性:

  • color
  • border-styleborder-radius
  • visibility
  • text-decoration
  • backgroundbackground-imagebackground-positionbackground-repeatbackground-size
  • outlineoutline-coloroutline-styleoutline-width
  • box-shadow

我们来看一个例子:

var bstyle = document.body.style; // cache
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; //  再一次的 reflow 和 repaint
bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint
bstyle.fontSize = "2em"; // reflow, repaint
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode("dude!"));

当然,我们的浏览器是聪明的,它不会像上面那样,你每改一次样式,它就 reflow 或 repaint 一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。但是有些情况浏览器是不会这么做的,比如:resize 窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行 reflow。

但是有些时候,我们的脚本会阻止浏览器这么干,比如:如果我们请求下面的一些 DOM 值:

减少重绘与重排:

重绘和回流在实际开发中是很难避免的,我们能做的就是尽量减少这种行为的发生。

  • js 尽量少访问 dom 节点和 css 属性,尽量不要过多的频繁的去增加,修改,删除元素,因为这可能会频繁的导致页面 reflow,可以先把该 dom 节点抽离到内存中进行复杂的操作然后再 display 到页面上。(虚拟 DOM)

  • 减少不必要的 DOM 层级(DOM depth)。改变 DOM 树中的一级会导致所有层级的改变,上至根部,下至被改变节点的子节点。这导致大量时间耗费在执行 reflow 上面。

  • 不要通过父级来改变子元素样式,最好直接改变子元素样式,改变子元素样式尽可能不要影响父元素和兄弟元素的大小和尺寸

  • 尽量通过 class 来设计元素样式,切忌用 style 多次操作单个属性

    // bad
    var left = 10,
      top = 10;
    el.style.left = left + "px";
    el.style.top = top + "px";
    // Good
    el.className += " theclassname";
    // Good
    el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • 尽可能的为产生动画的 HTML 元素使用 fixedabsoluteposition ,那么修改他们的 CSS 是不会 Reflow 的。

  • img 标签要设置高宽,以减少重绘重排

  • 把 DOM 离线后修改,如将一个 dom 脱离文档流,比如 display:none ,再修改属性,这里只发生一次回流。

  • 尽量用 transform 来做形变和位移,不会造成回流

  • 权衡速度的平滑。比如实现一个动画,以 1 个像素为单位移动这样最平滑,但 reflow 就会过于频繁,CPU 很快就会被完全占用。如果以 3 个像素为单位移动就会好很多。

  • 不要用 tables 布局的另一个原因就是 tables 中某个元素一旦触发 reflow 就会导致 table 里所有的其它元素 reflow。在适合用 table 的场合,可以设置 table-layout 为 auto 或 fixed,

  • 避免不必要的复杂的 CSS 选择器,尤其是后代选择器(descendant selectors),因为为了匹配选择器将耗费更多的 CPU。

PS: display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发现位置变化。

总结一下:

重排:元素的尺寸变了、位置变了

重绘:元素的颜色、背景、边框、轮廓变了,但是,元素的几何尺寸没有变。

Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。

# 页面加载缓慢的原因

browser-rendering

backend-generation

wpo-front-backend

# 浏览器部分

  • 网络层面

    1. 过多的 HTTP 请求

      打开一个网页的时候,后台程序的响应并不所需太多时间,等待的时间主要花费在下载网页元素上了,即 HTML、CSS、JavaScript、Flash、图片等。据统计,每增加一个元素,网页载入的时间就会增加 25-40 毫秒(具体取决于用户的带宽情况)。

    2. 资源访问带宽小

      两方面,一方面是客户端的带宽,一方面是服务器端的带宽。

    3. 网页元素 (图片、视频、样式) 太大

  • 浏览器渲染层面

    1. 渲染阻塞:

      浏览器想要渲染一个页面就必须先构建出 DOM 树与 CSSOM 树,如果 HTMLCSS 文件结构非常庞大与复杂,这显然会给页面加载速度带来严重影响。

      所谓渲染阻塞资源,即是对该资源发送请求后还需要先构建对应的 DOM 树或 CSSOM 树,这种行为显然会延迟渲染操作的开始时间。

      JS 阻塞与 CSS 阻塞:

      HTML、CSS、JavaScript 都是会对渲染产生阻塞的资源,HTML 是必需的(没有 DOM 还谈何渲染),但还可以从 CSS 与 JavaScript 着手优化,尽可能地减少阻塞的产生。

    2. 重复渲染

    3. DNS 解析

  • 服务端层面

    1. 硬件配置低:这个是双向的
    2. 服务器软件,比如防火墙、内网策略等
    3. 未对 Nginx 这类 web 服务器进行配置优化
    4. CPU 占满、数据库未优化
    5. 代码问题,代码效率,代码性能
    6. 包含了过多的分析类工具

# 代码部分

  • 构建层面

    未对代码进行打包、压缩、兼容性优化。

    未合并重复的请求、代码。

  • 编码层面

    没有良好的编码习惯,错误的编排 JS 与 CSS

    for 循环、迭代、同步、重定向、阻塞请求

    未删除重复、无用的代码

    未对逻辑业务复杂的代码进行重构,了解设计模式,对业务进行疏理

  • 机制 (SSR,英文 Server Side Render:服务器端渲染)

    未加入 Async 异步机制

    未思考页面加载、用户体验

  • 规范

    CSS 规范

    HTML 规范 / HTML5 规范

    Airbnb 代码规范等。

# 优化原则

# 尽量减少 HTTP 请求

在浏览器 (客户端) 和服务器发生通信时,就已经消耗了大量的时间,尤其是在网络情况比较糟糕的时候,这个问题尤其的突出。

一个正常 HTTP 请求的流程简述:如在浏览器中输入 "www.xxxxxx.com" 并按下回车,浏览器再与这个 URL 指向的服务器建立连接,然后浏览器才能向服务器发送请求信息,服务器在接受到请求的信息后再返回相应的信息,浏览器接收到来自服务器的应答信息后,对这些数据解释执行。

而当我们请求的网页文件中有很多图片、CSS、JS 甚至音乐等信息时,将会频繁的与服务器建立连接,与释放连接,这必定会造成资源的浪费,且每个 HTTP 请求都会对服务器和浏览器产生性能负担。

网速相同的条件下,下载一个 100KB 的图片比下载两个 50KB 的图片要耗费的网络资源更多。所以,请减少 HTTP 请求。

具体的方法:

  1. 组合文件,优化图片,使用 sprites 设计风格:将背景图片合并成一个文件,通过 background-imagebackground-position 控制显示;

    确保您的图像不大于它们所需的图像,它们采用正确的文件格式(PNG 通常更适用于少于 16 种颜色的图形,而 JPEG 通常更适合照片)并且它们是针对 Web 压缩的。

    使用 CSS sprites 在网站上经常使用的图像创建模板,如按钮和图标。CSS sprites 将您的图像组合成一个大图像,一次加载所有(这意味着更少的 HTTP 请求),然后只显示您想要显示的部分。这意味着您通过不让用户等待加载多个图像来节省加载时间。

    有一些在线工具:

    • Sprite Cow
    • Spritebox
  2. 肉联图片,使用 data:URL 方案将图像数据嵌入实际页面中。这可以增加 HTML 文档的大小。将内嵌图像组合到(缓存的)样式表中是一种减少 HTTP 请求并避免增加页面大小的方法。

  3. 简化页面的设计

# 使用内容传送网络 CDN

内容分发网络(CDN),也称为内容传送网络,是用于分发传送内容的负载的服务器网络。从本质上讲,您网站的副本存储在多个地理位置不同的数据中心,以便用户可以更快,更可靠地访问您的网站。

对于初创公司和私人网站来说,CDN 服务的成本可能过高,但随着您的目标受众变得越来越大并变得更加全球化,CDN 对于实现快速响应时间是必要的。

请记住,最终用户响应时间的 80-90%用于下载页面中的所有组件:图像,样式表,脚本,Flash 等。这是 Performance Golden Rule。而不是从重新设计应用程序架构的艰巨任务开始,最好首先分散您的静态内容。这不仅可以大大缩短响应时间,而且由于内容交付网络,它更容易实现。

内容传送网络(CDN)是分布在多个位置的 Web 服务器的集合,以更有效地向用户传送内容。选择用于向特定用户传送内容的服务器通常基于网络接近度的度量。例如,选择具有最少网络跳数的服务器或具有最快响应时间的服务器。

CDN 服务商有很多,这里就不再多说:专门做 CDN 服务器的蓝讯、网宿、帝联、快网,还有阿里云、腾讯云、华为云等。国外如 Akamai TechnologiesEdgeCast level3

除了去购买一些 CDN 服务商的服务以外,对于大多数开发者,可以使用公共 CDN 网络上的资源,如以下的方式去使用 CDN 加速:

<!-- google -->
<script
  type="text/javascript"
  src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"
></script>
<!-- cdnjs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<!-- qiniu cloud -->
<script src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js"></script>
<!-- other cdn -->
<script
  crossorigin="anonymous"
  integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
  src="https://lib.baomitu.com/jquery/3.4.1/jquery.min.js"
></script>

# 避免空 src 或者是 href 值

空的 src 和 href 都会导致多余的 HTTP 请求,虽然不影响加载时间,但是会对服务器产生不必要的流量和压力。浏览器仍然会向服务器发起一个 HTTP 请求:

  • IE 向页面所在的目录发送请求
  • Safari、Chrome、Firefox 向页面本身发送请求
  • Opera 不执行任何操作

空的 src 的 image 严重的以至于影响整个网站的用户体验,空 src 产生请求的后果不容小憩:

  • 给服务器造成意外的流量负担,尤其时日 PV 较大时;
  • 浪费服务器计算资源;
  • 可能产生报错。

有两种形式:

  1. HTML 形式

    <img src="" /> <a href=""></a>
  2. JavaScript 形式

    var img = new Image();
    img.src = "";

解决办法:

  1. 删除空的 srchref 标签
  2. a 标签的 href 属性,连接到实际的页面:
<a href="#"></a>
<a href="#nogo"></a>
<a href="##"></a>
<a href="###"></a>
<a href="void(0);"></a>
<a href="void(0)"></a>
<a href=";"></a>
<a href=""></a>
  1. 禁止跳转,添加 cursor:pointer 样式
<style>
  a {
    cursor: pointer;
  }
</style>
<a>点击一</a>
<a onclick="doSomething()">点击二</a>
  1. a 标签创建一个带有描述信息的 href 属性,并监控 click 事件调用 preventDefault() 函数。
<a href="#Something_De scriptive" id="my_id">Trigger</a>
<script>
  $("#my_id").click(function (e) {
    e.preventDefault(); // 取消单击事件的默认动作以阻止链接的跳转。
    //  其他的代码
  });
</script>

优点:

  • <a> 够响应键盘事件并获得焦点(从而屏幕阅读器能够读出背后的内容,增强可访问性)
  • 优雅降级,在网络连接很差,还没有加载到 CSS 的时候, <a> 依然有手型与正常的 link 样式。

# gzip 的组件

所有现代浏览器都支持 gzip 压缩并会为所有 HTTP 请求自动协商此类压缩。启用 gzip 压缩可大幅缩减所传输的响应的大小(最多可缩减 90%),从而显著缩短下载相应资源所需的时间、减少客户端的流量消耗并加快网页的首次呈现速度。

从 HTTP / 1.1 开始,Web 客户端表示支持使用 HTTP 请求中的 Accept-Encoding 标头进行压缩。

Accept-Encoding:gzip,deflate

压缩包括 XML 和 JSON 在内的任何文本响应都是值得的。不应对图像和 PDF 文件进行 gzip 压缩,因为它们已经过压缩。试图对它们进行 gzip 不仅会浪费 CPU,还可能会增加文件大小。

比如,在 nginx 中开启 gzip 压缩:

# 开启 gzip
gzip on;
# 启用 gzip 压缩的最小文件,小于设置值的文件将不会压缩
gzip_min_length 1k;
# gzip 压缩级别,1-10,数字越大压缩的越好,也越占用 CPU 时间,后面会有详细说明
gzip_comp_level 2;
# 进行压缩的文件类型。javascript 有多种形式。其中的值可以在 mime.types 文件中找到。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml;
# 是否在 http header 中添加 Vary: Accept-Encoding,建议开启
gzip_vary on;
# 禁用 IE 6 gzip
gzip_disable "MSIE [1-6]\.";

其他 web 容器启动 gzip 的方法:

  • Apache:使用 mod_deflate
  • Nginx:使用 ngx_http_gzip_module
  • IIS:配置 HTTP 压缩功能

# CSS 放在顶部,JS 放在底部

把 Javascript 脚本在底部,删除阻止渲染的 JavaScript
在 HTML 文件 <body> 中指定外部样式表和内联样式块可能对浏览器的渲染性能产生不利影响。

  1. 浏览器阻塞渲染网页直到所有外部的样式表都已被下载。
  2. (用 <style> 标记指定的)内联样式块可能会导致 reflows 和页面跳动。
    因此,把外部样式表和内联样式块放在页面的 <head> 中是很重要的。通过确保样式表首先被下载和解析,可以让浏览器逐步渲染页面。

具体做法:

  • 将内联样式块和 <link> 元素从页面 <body> 移动到页面 <head> 中。

    HTML 4.01 规范(第 12.3 节)规定,始终把使用 <link> 标签的外部样式表放在 <head> 部分里,还要确保您指定的样式有正确的顺序。

  • <style> 区块放在 <head> 部分里。

  • 使用 css 媒体类型

如果可以让 CSS 资源只在特定条件下使用,这样这些资源就可以在首次加载时先不进行构建 CSSOM 树,只有在符合特定条件时,才会让浏览器进行阻塞渲染然后构建 CSSOM 树。

CSS 的媒体查询正是用来实现这个功能的,它由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。

<!-- 没有使用媒体查询,这个 css 资源会阻塞渲染  -->
<link href="style.css" rel="stylesheet" />
<!-- all 是默认类型,它和不设置媒体查询的效果是一样的 -->
<link href="style.css" rel="stylesheet" media="all" />
<!-- 动态媒体查询, 将在网页加载时计算。
根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。-->
<link href="portrait.css" rel="stylesheet" media="orientation:portrait" />
<!-- 只在打印网页时应用,因此网页首次在浏览器中加载时,它不会阻塞渲染。 -->
<link href="print.css" rel="stylesheet" media="print" />

使用媒体查询可以让 CSS 资源不在首次加载中阻塞渲染,但不管是哪种 CSS 资源它们的下载请求都不会被忽略,浏览器仍然会先下载 CSS 文件。

把 Javascript 脚本在底部,删除阻止渲染的 JavaScript

浏览器必须通过在呈现页面之前解析 HTML 来构建 DOM 树。如果您的浏览器在此过程中遇到脚本,则必须先停止并执行它,然后才能继续。

具体做法:

将脚本定义或引用放置到 <body> 底部。
<script defer="defer"> defer 属性规定是否对脚本执行进行延迟, 脚本将在页面完成解析时执行。

请输入图片描述

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

此图告诉我们以下几个要点:

  1. deferasync 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
  2. 它俩的差别在于脚本下载完之后何时执行,显然 defer * 是最接近我们对于应用脚本加载和执行的要求的
  3. 关于 defer ,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
  4. async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
  5. 仔细想想, async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics

# 减少 DNS 查找

用户访问网站的过程如下:

  1. 在地址栏输入网站地址,如 www.example.com;

  2. 本地 DNS 得到这个请求,查询本地 DNS 缓存,如果有这条记录,则直接返回对应的 IP;否则,请求网络上的 DNS 服务器,得到相应的 IP,返回给客户机,并缓存这条记录;

  3. 浏览器向得到的 IP 发起建立连接请求,得到响应后建立连接,请求数据;

  4. Server 端计算所需数据,并返回给 client 端;

  5. client 端,即浏览器,解析数据并显示在浏览器窗口中,至此,请求完成。

在 一次请求中,DNS 解析可以占到请求时间的三分之一左右(这点有待验证),所以如果可以缩短 DNS 解析时间,就可以加快页面的打开速度。

缩短 DNS 解析的 方法可以通过延长 DNS 缓存的时间选用更快的 DNS Server减少域名总数(例如原来有 5 个 img server,分别为 img1.xxx.comimg5.xxx.com,则现在可以减少到 3 个)等等,但是减少域名个数又会降低资源并行下载的数量, 因为同一域名最多可以并行下载两个资源,所以这里需要一个折衷方案,作者的建议就是将资源分布在大于等于 2 但小于等于 4 个域名上(这个也有待验证,例如针 对多大的系统选用多少个域名是最合理的等)。

使用 DNS 预解析

这里会有一些兼容性问题,可以参见:http://caniuse.com/#feat=link-rel-dns-prefetch

sdfdsf

在网页体验中我们常会遇到这种情况,即在调用百度联盟、谷歌联盟以及当前网页所在域名外的域名文件时会遇到请求延时非常严重的情况。那么有没有方法去解决这种请求严重延时的现象呢?

一般来说这种延时的原因不会是对方网站带宽或者负载的原因,那么到底是什么导致了这种情况呢。湛蓝试着进行推测,假设是 DNS 的问题,因为 DNS 解析速度很可能是造成资源延时的最大原因。在页面 header 中添加了以下代码(用以 DNS 预解析):

<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
<link rel="dns-prefetch" href="http://nsclick.baidu.com" />
<link rel="dns-prefetch" href="http://hm.baidu.com" />
<link rel="dns-prefetch" href="http://eiv.baidu.com" />

dns-prefetch 需慎用,多页面重复 DNS 预解析会增加重复 DNS 查询次数。

使用场景:

  1. 新用户访问,后端可以通过 Cookie 判断是否为首次进入站点,对于这类用户,DNS Prefetch 可以比较明显地提升访问速度
  2. 登录页,提前在页面上进行下一跳页用到资源的 DNS Prefetch
  3. 页面中的静态资源在不同的 domain 下,如 CSS、JS、图片等文件
  4. 电商网站的商品页大量载入不同 domain 下的商品图,如淘宝
  5. 手机网页
  6. 大型网站
  7. js 或服务端重定向

Chrome 中的一些指令:

  • chrome://histograms/DNS.PrefetchQueue :查看队列状态
  • chrome://histograms/DNS :查看从浏览器启动到上一页的 DNS 记录
  • chrome://dns :查看个域名 DNS 统计
  • chrome://net-internals/#dns :清除 host 缓存

# 压缩资源

  • 压缩 js,css,image

    通过对外部资源进行压缩可以大幅度地减少浏览器需要下载的资源量,它会减少关键路径长度与关键字节,使页面的加载速度变得更快。

    对数据进行压缩其实就是使用更少的位数来对数据进行重编码。如今有非常多的压缩算法,且每一个的作用领域也各不相同,它们的复杂度也不相同,不过在这里我不会讲压缩算法的细节,感兴趣的朋友可以自己 Google。

    在对 HTMLCSSJavaScript 这些文件进行压缩之前,还需要先进行一次冗余压缩。所谓冗余压缩,就是去除多余的字符,例如注释、空格符和换行符。这些字符对于程序员是有用的,毕竟没有格式化的代码可读性是非常恐怖的,但它们对于浏览器是没有任何意义的,去除这些冗余可以减少文件的数据量。

    小图片采用 base64 的格式,直接嵌入代码中,可以帮我们减少 http 请求,但是同样,这个会造成我们代码的提及变大。请求的速度会减慢,这个需要平衡。

    常用工具:webpack,gulp

    浏览器接收到服务器返回的 HTML、CSS 和 JavaScript 字节数据并对其进行解析和转变成像素的渲染过程被称为 关键渲染路径 。通过优化关键渲染路径即可以缩短浏览器渲染页面的时间。

  • 删除重复的脚本,类似 tree-shaking

    在一个页面中重复引用一个脚本可能存在的问题:浏览器会重复下载并执行脚本文件。

  • 使用合适大小的图片

    如果你只需要一个小图,就不要传一个大图。例如你实际需要显示的是一个 60x60 的头像,就不要传一个 100x100 的然后再通过设置宽高将它缩小为 60x60 的。原因很简单,这样会消耗不必要的带宽和系统资源。

  • 减少 DOM 元素的数量

    复杂页面意味着要下载更多字节,这也意味着 JavaScript 中的 DOM 访问速度更慢。如果您想要添加事件处理程序,例如,在页面上循环 500 或 5000 个 DOM 元素,则会有所不同。

  • 使用异步加载,async、defer

    服务端渲染也是一种减少浏览器资源消耗,减少页面重排重绘,也是一种现行 MVVM 框架单页应用最主要的 SEO 优化手段。

# 避免 3xx/4xx

避免重定向

3xx 是重定向相关的 HTTP 响应代码

重定向的意思是,用户的原始请求(例如请求 A)被重定向到其他的请求(例如请求 B)。

每次页面重定向到另一个页面时,您的访问者都会面临等待 HTTP 请求 - 响应周期完成的额外时间。例如,如果移动重定向模式如下所示:

example.com - > www.example.com - > m.example.com - > m.example.com/home ,这两个额外重定向中的每一个都会使您的页面成为可能加载速度慢。

HTTP 重定向通过 301 / 302 状态码实现。

HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html

301 Moved Permanently ,这个状态码标识用户所请求的资源被移动到了另外的位置,客户端接收到此响应后,需要发起另外一个请求去下载所需的资源。

302 Found ,这个状态码标识用户所请求的资源被找到了,但不在原始位置,服务器会回复其他的一个位置,客户端收到此响应后,也需要发起另外一个请求去下载所需的资源。

客户端收到服务器的重定向响应后,会根据响应头中 Location 的地址再次发送请求。重定向会影响用户体验,尤其是多次重定向时,用户在一段时间内看不到任何内容,只看到浏览器进度条一直在刷新。

有时重定向无法避免,在糟糕也比抛出 404 好。虽然通过 HTML meta refresh 和 JavaScript 也能实现,但首选 HTTP 3xx 跳转,以保证浏览器「后退」功能正常工作(也利于 SEO)。

常见的优化办法:

  • 最浪费的重定向经常发生、而且很容易被忽略:URL 末尾应该添加 / 但未添加。比如,访问 http://astrology.yahoo.com/astrology 将被 301 重定向到 http://astrology.yahoo.com/astrology/ (注意末尾的 / )。如果使用 Apache,可以通过 Aliasmod_rewriteDirectorySlash 解决这个问题。

  • 网站域名变更:CNAME 结合 Aliasmod_rewrite 或者其他服务器类似功能实现跳转。

  • 在定义链接地址的 href 属性的时候,尽量使用最完整的、直接的地址。例如:
    使用 www.cnblogs.com 而不是 cnblogs.com
    使用 cn.bing.com 而不是 bing.com
    使用 www.google.com.hk 而不是 google.com
    使用 www.mysite.com/products/ 而不是 www.mysite.com/products

避免 404 浏览器找不到资源的情况发生

发出 HTTP 请求并获得无用的响应(即 404 Not Found)是完全没必要的。特别糟糕的是当外部 JavaScript 的链接错误并且结果是 404 时。首先,此下载将阻止并行下载。接下来,浏览器可能会尝试解析 404 响应主体,就像它是 JavaScript 代码一样,这样就带来的性能的浪费。

404 的影响:

有时候,404 错误发生了,用户可能根本没有感觉到。例如

  • 例如请求 favicon.ico 文件,或者请求了某个不存在的脚本文件、样式表、图片文件,页面还是会按照正常的方式进行呈现。
  • 丢失的脚本文件、样式表、图片文件,会导致页面的某些行为、界面效果出现异常(也可能不是很明显)
  • 最大的问题可能是性能方面的影响。尤其是如果请求一个不存在的脚本文件,因为浏览器在请求脚本文件的时候,即便是返回 404,它也会尝试去按照 Javascript 的方式解析响应中的内容。这无疑会增加很多处理的时间,而因为该文件不存在,所以这些都是无用功。

看得到的影响:

  • 如果用户请求的某个页面不存在,那么他将收到明确的回应
  • 默认情况下,他将收到一个标准的错误页面(请注意:不少用户会被这个页面吓到)

常见的优化办法:

404 意味着 Not Found,意思是说未找到资源。既然如此,那么至少会有两种原因导致 404 错误:

  • 该资源按理说是要有,但我们没有提供。用户按照正常的方式来请求,所以资源找不到。
    • 为网站提供 favicon.ico 这种经常可能会被忽略的资源
    • 使用一些检查工具:比如 Link checker
  • 该资源本来就不存在,用户按照不正常的方式来请求,当然还是找不到。
    • 避免用户收藏绝对地址,给后期更新带来隐患。可以使用地址 Rewrite 来重写,或者在设计阶段定义一些灵活友好的地址
    • 使用 Routing 技术,配置路由规则。

# AJAX 优化

使 Ajax 可缓存

AJAX=Asynchronous JavaScript And XML,AJAX 不是新的编程语言,而是一种使用现有标准的新方法。
AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下。

由于 AJAX 其实也是需要发起请求,然后服务器执行,并将结果(通常是 JSON 格式的)发送给浏览器进行最后的呈现或者处理,所以对于网站设计优化的角度而言,我们同样需要考虑对这些请求,是否可以尽可能的利用到缓存的功能来提高性能。

对于 AJAX 而言,有一些特殊性,并不是所有的 AJAX 请求都是可以缓存的。

  1. POST 的请求,是不可以在客户端缓存的,每次请求都需要发送给服务器进行处理,每次都会返回状态码 200。(这里可以优化的是,服务器端对数据进行缓存,以便提高处理速度)
  2. GET 的请求,是可以(并且默认)在客户端进行缓存的,除非指定了不同的地址,否则同一地址的 AJAX 请求,不会重复再服务器执行,而是返回 304。

有的时候,我们可能希望 GET 请求不被缓存,有几种做法来达到这样的目的。

  1. 每次调用的时候,请求不同的地址(可以在原始地址后面添加一个随机的号码)。
  2. 如果你所使用的是 jquery 的话,则可以考虑禁用 AJAX 的缓存。
$.ajaxSetup({ cache: false });

axios 中

var config = {
  headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" },
};
axios.get("/post", config);

使用 GET 的 Ajax 请求

在使用 XMLHttpRequest(目前的 AJAX 都是基于它实现的)的时候,浏览器中的 POST 实现为两步走的过程,首先发送头部信息,然后再发送数据。但如果是使用 GET 的话,就只有一个 TCP 的包发送出去(除非有大量的 Cookie),这样无疑可以提高性能。

但是 get 有容量限制,大于 2K (对 get 的限制 IE 是 2K,firefox、chrome 是 4K) 的内容只能用 post。

  • 减小 cookie 大小

    减小 cookie 的大小,因为在发请求时浏览器会将 cookie 信息发送到 server 端,所以应该只在 cookie 中存必要的信息且越长度越小越好。在 写 cookie 的时候要记得给 cookie 设置一个合理的过期时间及域

  • 对一些静态资源不需要使用 cookie 的单独设置域

    当浏览器发出静态图像请求并将 cookie 与请求一起发送时,服务器对这些 cookie 没有任何用处。因此,他们只会毫无理由地创建网络流量。您应该确保使用无 cookie 请求请求静态组件。创建一个子域并在那里托管所有静态组件。

    如果您的域名是 www.example.org ,您可以托管您的静态组件 static.example.org 。但是,如果您已经在顶级域上设置了 cookie example.org 而不是 www.example.org ,则所有请求都 static.example.org 将包含这些 cookie。在这种情况下,您可以购买一个全新的域,在那里托管您的静态组件,并保持此域无 cookie。雅虎 用途 yimg.com ,YouTube 使用 ytimg.com ,亚马逊使用 images-amazon.com 等。

    在无 cookie 域上托管静态组件的另一个好处是,某些代理可能拒绝缓存使用 cookie 请求的组件。在相关说明中,如果您想知道是否应该使用 example.orgwww.example.org 作为主页,请考虑 cookie 的影响。

    省略 www 会让您别无选择,只能写入 cookie *.example.org ,因此出于性能原因,最好使用 www 子域并将 cookie 写入该子域。

归类一下:

  • 去除不必要的 Cookie;
  • 尽量压缩 Cookie 大小;
  • 注意设置 Cookie 的 domain 级别,如无必要,不要影响到 sub-domain;
  • 设置合适的过期时间。

# 利用缓存

浏览器会缓存大量信息(样式表,图像,JavaScript 文件等),以便当访问者返回您的站点时,浏览器不必重新加载整个页面。然后设置 “expires” 标题,以表示希望缓存该信息的时间。在许多情况下,除非您的网站设计经常更改,否则一年是合理的时间段。

利用浏览器缓存 ,为链接或者资源,添加 Expires 或 Cache-Control 头

  • 对于静态组件:通过设置远期未来 Expires 标头实现 “永不过期” 策略
  • 对于动态组件:使用适当的 Cache-Control 标头来帮助浏览器处理条件请求

格式:

Expires = "Expires" ":" HTTP-date
# e.g.
Expires: Thu, 01 Dec 1994 16:00:00 GMT
#(必须是 GMT 格式)

通过 HTTP 的 META 设置 expires 和 cache-control

<meta http-equiv="Cache-Control" content="max-age=7200" />
<meta http-equiv="Expires" content="Mon, 01 Aug 2019 00:00:00 GMT" />

上述设置仅为举例,实际使用其一即可。这样写的话仅对该网页有效,对网页中的图片或其他请求无效,并不会做任何 cache
这样客户端的请求就多了,尽管只是检查 Last-modified 状态的东西,但是请求一多对浏览速度必定有影响。

Cache-Control 的参数包括:

  • max-age=[单位:秒 seconds] — 设置缓存最大的有效时间。类似于 Expires , 但是这个参数定义的是时间大小(比如:60)而不是确定的时间点。单位是 [秒 seconds].
  • s-maxage=[单位:秒 seconds] — 类似于 max-age , 但是它只用于公享缓存 (e.g., proxy) .
  • public — 响应会被缓存,并且在多用户间共享。正常情况,如果要求 HTTP 认证,响应会自动设置为 private.
  • private — 响应只能够作为私有的缓存 (e.g., 在一个浏览器中),不能再用户间共享。
  • no-cache — 响应不会被缓存,而是实时向服务器端请求资源。这一点很有用,这对保证 HTTP 认证能够严格地禁止缓存以保证安全性很有用(这是指页面与 public 结合使用的情况下). 既没有牺牲缓存的效率,又能保证安全。
  • no-store — 在任何条件下,响应都不会被缓存,并且不会被写入到客户端的磁盘里,这也是基于安全考虑的某些敏感的响应才会使用这个。
  • must-revalidate — 响应在特定条件下会被重用,以满足接下来的请求,但是它必须到服务器端去验证它是不是仍然是最新的。
  • proxy-revalidate — 类似于 must-revalidate , 但不适用于代理缓存.

把 css 与 js 使用外联的方式进行使用

建议将 css 和 js 以外联方式引用以充分利用 cache ,只有一个例外就是针对首页,首页可以采用内联来减少 http 请求数,加快页面显示速度,文中说首 页内联的原因是首页被人们访问的次数不会太多,而且样式一般比较特殊,不像其它页面一样有相同的页面模板(因为就有相同的样式部分),所以首页的样式可以 内联。并且应该在首页加载完成之后,再在后台动态加载后续页面的 css 和 js,以提高后续页面的访问速度。

如果站点上的用户每个会话有多个页面查看,并且您的许多页面重复使用相同的脚本和样式表,则缓存的外部文件可能会带来更大的潜在好处。

对于现在的单页面的应用,更适合于动态加载的方案

缓存 favicon.ico,并设置最好小于 1k

favicon.ico 是一个保留在服务器根目录中的图片,为了减轻拥有 favicon.ico 的带来的性能问题,请确保:

  • 优化它的大小,最好不到 1K。
  • 设置 Expires 标头,使其缓存。

配置的 ETag

ETag,全程为: Entity Tag ,意思是实体标签,它属于 HTTP 协议的一部分,也就是所有的 Web 服务器都应该支持这个特性。它的作用是用一个特殊的字符串来标识某个资源的 “版本”,客户端(浏览器)请求的时候,比较 ETag 如果一致,则表示该资源并没有被修改过,客户端(浏览器)可以使用自己缓存的版本,避免重复下载。
它比 last-modified date 更具有弹性,例如某个文件在 1 秒内修改了 10 次,Etag 可以综合 Inode (文件的索引节点 (inode) 数),MTime (修改时间) 和 Size 来精准的进行判断,避开 UNIX 记录 MTime 只能精确到秒的问题。 服务器集群使用,可取后两个参数。使用 ETags 减少 Web 应用带宽和负载。

响应标优势 和特点劣势 和可能的问题
Expires- HTTP 1.0 就有,简单易用。 - 服务器通过这个 Header 告诉浏览器,某资源直到某个时间才会过期,所以在没有过期之前,浏览器就直接使用本地的缓存了。- 因为这是时间是由服务器发送的(UTC),但如果服务器时间和客户端事件存在不一致,可能会有些问题。 - 可能存在版本的问题,因为如果在到期之前修改过了,客户端是不会知道的。 - Cache-Control 中的 max-age 可以实现类似的效果,但更加好,因为 max-age 是一个以秒为单位的时间数,而不是具体的时间,所以不存在上面提到的第一个问题。
Cache-Contro- 服务器通过一个 Header(Last-Modified)告诉浏览器,某资源最后修改的时间。 - 浏览器在请求的时候,包含一个 Header(If-Modified-Since), 然后服务器可以进行比较,如果在该时间后没有修改过,则返回 304。 - 它比 Expires 多很多选项设置- Last-Modified 也是一个时间,但该时间只能精确到秒,如果在同一个秒中有多次修改(这个在现在的环境下应该确实是可能的),则可能会发生问题。
ETag- 可以更加精确地判断资源是否被修改,因为它不是一个时间值,而是对时间经过处理的一个长整型数值(当然具体算法我们目前还不得而知)。 - 浏览器发起新请求时需要包含 If-None-Match。- 如果部署在服务器场环境中,配置不当的话,可能每个服务器会对相同的资源生成不一样的 ETag,这样就增加了重复下载的可能性。

我们来看一个真实的例子:

/images/image-20190619145105413

其他的一些缓存使用:

  • 使用本地缓存,缓存部分常用用户数据、公开数据
  • 缓存机制设计:缓存 + 过期时间

总结一下:

  • 添加 Expires 或 Cache-Control
  • Etag
  • 缓存 favicon.ico
  • 外联 js/css

# 缩短服务器响应时间

服务器响应时间受到访问流量,每个页面使用的资源,服务器使用的软件以及硬件本身的影响。要改善服务器响应时间,请查找性能瓶颈,如慢速数据库查询,慢速路由或缺少足够的内存并修复它们。最佳服务器响应时间低于 200 毫秒。

很多潜在因素都可能会延缓服务器响应,例如缓慢的应用逻辑、缓慢的数据库查询、缓慢的路由、框架、库、资源 CPU 不足或内存不足。您需要充分考虑所有这些因素,才能改善服务器的响应用时。 若想找出服务器响应用时过长的原因,首先要进行衡量。然后,准备好相关数据,并参阅有关如何解决该问题的相应指导。当解决问题后,您必须继续衡量服务器响应用时,并设法应对任何会在将来出现的性能瓶颈问题。

  1. 收集并检查现有性能和数据。若无可用内容,请使用自动化的网络应用监测解决方案(市面上有托管的开源版本,适用于大多数平台)进行评估,或添加自定义的方法。
  2. 找出并修复首要的性能瓶颈问题。如果 z 您使用的是热门网页框架或内容管理平台,请参阅与性能优化最佳做法相关的文档。
  3. 监测并提醒任何会在将来出现的性能衰退问题!

通常来说,服务器的响应时间由数据库的问题居多,还有错误的程序设计 (比如:烂用 for 循环去查数据库)。

# 优化工具

# Google Page Speed

Google Page Speed 以帮助开发人员和网站所有者确保他们的网页尽可能顺畅,快速地运行。

Google Page Speed 特点:

  • 在 CSS 方面更加注重细节,对于 CSS 选择器,Google Page Speed 提供了大量有用的建议
  • 页面速度活动功能类似于加载计时器评估,但它实时提供图表,因此可以在任何给定时间准确查看网站上发生的情况。
  • 自动优化:使用 PageSpeed 模块,可以将 Google 与您的 Nginx 或 Apache 网站服务器集成,以自动优化您的网站。
  • 优化库集成:可以找到 PageSpeed 模块背后优化建议,允许设计和构建自己的工具。

对于某些人来说,Google Page Speed 将是显而易见的选择,因为它可以与其他对维护网站性能至关重要的 Google 工具一起使用,例如 Google Analytics。

# JsPerf & Benchmark.js

经常看到项目中有一个 benchmark 的文件夹,那么这个 benchmark 是干嘛用的?

JsPerf 在线:https://jsperf.com/

JsPerf Github 地址:https://github.com/jsperf/jsperf.com

Benchmark.js 网址:https://github.com/bestiejs/benchmark.js

BenchMark.js 可以本地安装,而 JsPerf.js 是线上使用的。

HTML 中引入:

<script src="lodash.js"></script>
<script src="platform.js"></script>
<script src="benchmark.js"></script>

或者使用 npm :

$ {sudo -H} npm i -g npm
$ npm i --save benchmark

下面是一个具体的用例:

Reg vs IndexOf

var suite = new Benchmark.Suite();
// add tests
suite
  .add("RegExp#test", function () {
    /o/.test("Hello World!");
  })
  .add("String#indexOf", function () {
    "Hello World!".indexOf("o") > -1;
  })
  // add listeners
  .on("cycle", function (event) {
    console.log(String(event.target));
  })
  .on("complete", function () {
    console.log("Fastest is " + this.filter("fastest").map("name"));
  })
  // run async
  .run({ async: true });
// logs:
// => RegExp#test x 4,161,532 +-0.99% (59 cycles)
// => String#indexOf x 6,139,623 +-1.00% (131 cycles)
// => Fastest is String#indexOf

再来看一个 call 与 apply 的对比:

var Benchmark = require("benchmark");
var suite = new Benchmark.Suite();
var arr1 = function (str) {
  return [].slice.apply(str);
};
var str2 = function (str) {
  return [].slice.call(str);
};
// 添加测试
suite
  .add("arr1", function () {
    arr1("test");
  })
  .add("str2", function () {
    str2("test");
  })
  // add listeners
  .on("cycle", function (event) {
    console.log(String(event.target));
  })
  .on("complete", function () {
    console.log("Fastest is " + this.filter("fastest").pluck("name"));
  })
  // run async
  .run({ async: true });

benckmark 是对 js 运行能效的一个对比测试,让你能够知道自己的 JS 代码中与其他方案对比的时候,是否有优化的空间。当然,这些新的方案,需要自己去编写去测试。同样,也可以在 JsPerf 的官网上去看其他的测试结果。

执行办法 :

npm init -y
npm install -S benchmark
touch test.js //Linux
node test.js

# 代码优化 —— 写出优质的代码

命题很大,但是我们要简化来谈一下。

代码优化,先要写出优质的代码,有几点原则:

  • 使用辅助应用对代码进行检查,Eslint 等
  • 使用预编译工具,如 less,sass,stylus。
  • 使用官方的开发规范:命名规范、注释、使用规范、文档规范
  • 写出逻辑上易于阅读的代码:设计模式、代码结构、文档编排 (目录结构)
  • 代码审查
  • 单元测试
  • 异常与错误机制

之后就是练习与阅读,阅读别人的代码,学习别人的精华,了解别人代码中的缺点。使用 JsPerf 或者 benchemark 去对比优化性能。

前端代码规范:Airbnb,github,Google,腾讯,阿里等。

# JsPerf

# JsPerf 使用简介

# 线上使用方案

需要使用 Github 进行登录:

/images/image-20190618210408261

点击 Login with Github to Create Test Cases :

/images/image-20190619103010414

登录之后,进入授权页面:

/images/image-20190619103140822

登录过后,就可以创建自己的测试用例啦:

/images/image-20190619143549467

# 线下使用方案:

环境准备:

  • Mysql
  • Nodejs

创建过程:

  1. 安装 Mysql

    mysql -uroot -e "CREATE DATABASE jsperf; GRANT ALL ON jsperf.* TO 'jsuser'@'localhost' IDENTIFIED BY 'jspass'; FLUSH PRIVILEGES;"
  2. Browserscope.org 注册一个 API key ,在 the settings page 页面中.

  3. 注册一个 new OAuth GitHub application. 使用 callback URL 留空。复制 "Client ID" 和 "Client Secret" 到 github 的页面上。

  4. 使用 npm install 安装

  5. 配置 .env 的文件

NODE_ENV=development
# from Prerequisites step 1
MYSQL_USER=jsuser
MYSQL_PASSWORD=jspass
MYSQL_DATABASE=jsperf
# from Prerequisites step 2
BROWSERSCOPE=
# from Prerequisites step 3
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK=http://localhost:3000
BELL_COOKIE_PASS=password-should-be-32-characters
COOKIE_PASS=password-should-be-32-characters
# customizable but not recommended for local development
# SCHEME=http
# DOMAIN=localhost
# PORT=3000
# MYSQL_HOST=localhost
# MYSQL_PORT=3306
# LOGGLY_TOKEN=
# LOGGLY_SUBDOMAIN=
# REDIS_HOST=
# REDIS_PORT=
# REDIS_PASSWORD=
  1. 运行 npm start

# 代码示例 for vs foreach

我们对比一下 for 与 foreach

screencapture-jsperf-2019-06-18-21_15_29

测试过程:

/images/image-20190618211820471

测试结果:

/images/image-20190618211915799

这里有一个更全面的对比结果:

https://jsperf.com/for-vs-foreach/75

/images/image-20190618213130251

/images/image-20190618213201840

还有一些比较有意思的测试:

可以通过浏览:https://jsperf.com/popular 进行查看

/images/image-20190619143653346

# 前端项目优化实践

我们来对比下 Date () 与 Moment () 中的 format

const Benchmark = require("benchmark");
const moment = require("moment");
Date.prototype.Format = function (fmt) {
  var o = {
    "M+": this.getMonth() + 1, // 月份
    "d+": this.getDate(), // 日
    "h+": this.getHours(), // 小时
    "m+": this.getMinutes(), // 分
    "s+": this.getSeconds(), // 秒
    "q+": Math.floor((this.getMonth() + 3) / 3), // 季度
    S: this.getMilliseconds(), // 毫秒
  };
  if (/(y+)/.test(fmt))
    fmt = fmt.replace(
      RegExp.$1,
      (this.getFullYear() + "").substr(4 - RegExp.$1.length)
    );
  for (var k in o) {
    if (new RegExp("(" + k + ")").test(fmt))
      fmt = fmt.replace(
        RegExp.$1,
        RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
      );
  }
  return fmt;
};
var suite = new Benchmark.Suite();
// add tests
suite
  .add("Date format", function () {
    console.log(new Date().Format("yyyy-MM-dd hh:mm:ss"));
  })
  .add("Moment format", function () {
    console.log(moment().format("YYYY-MM-DD HH:mm:ss"));
  })
  // add listeners
  .on("cycle", function (event) {
    console.log(String(event.target));
  })
  .on("complete", function () {
    console.log("Fastest is " + this.filter("fastest").map("name"));
  })
  // run async
  .run({ async: true });

输出结果 :

Moment format x 73,570 ops/sec ±4.93% (82 runs sampled)
Fastest is Moment format

# PageSpeed

# PageSpeed 简介

PageSpeed Insights 的 Chrome 扩展是由谷歌官方开发的一款可以分析页面载入的各个方面,包括资源、网络、DOM 以及时间线等等信息的插件,也提供在线访问方式。

# 使用 Google PageSpeed Insights

打开 https://developers.google.com/speed/pagespeed/insights/

输入需要分析的网址即可,这里面需要注意必须在公网可以访问到网址。

/images/image-20190619212956061

# 使用 Chrome 插件 / Mozila 插件

/images/image-20190619203556941

下载安装办法:

  1. 使用插件 ID: djfpadpbeemogokhllpiakdeadpoclan 到 https://chrome-extension-downloader.com/ 来下载

    打开:chrome://extensions/

    ctx 文件拖进去

  2. 如果有梯子,那么直接在商店里面搜索

插件使用:

/images/image-20190619212836235

# 前端项目优化实践

# 补充学习

# 编码、解码形成 DOM 树的过程

浏览器从网络或硬盘中获得 HTML 字节数据后会经过一个流程将字节解析为 DOM 树:

  • 编码: 先将 HTML 的原始字节数据转换为文件指定编码的字符。
  • 令牌化: 然后浏览器会根据 HTML 规范来将字符串转换成各种令牌(如 <html><body> 这样的标签以及标签中的字符串和属性等都会被转化为令牌,每个令牌具有特殊含义和一组规则)。令牌记录了标签的开始与结束,通过这个特性可以轻松判断一个标签是否为子标签(假设有 <html><body> 两个标签,当 <html> 标签的令牌还未遇到它的结束令牌 </html> 就遇见了 <body> 标签令牌,那么 <body> 就是 <html> 的子标签)。
  • 生成对象: 接下来每个令牌都会被转换成定义其属性和规则的对象(这个对象就是节点对象)。
  • 构建完毕: DOM 树构建完成,整个对象集合就像是一棵树形结构。可能有人会疑惑为什么 DOM 是一个树形结构,这是因为标签之间含有复杂的父子关系,树形结构正好可以诠释这个关系( CSSOS 同理,层叠样式也含有父子关系。例如: div p {font-size: 18px} ,会先寻找所有 p 标签并判断它的父标签是否为 div 之后才会决定要不要采用这个样式进行渲染)。

整个 DOM 树的构建过程其实就是: 字节 -> 字符 -> 令牌 -> 节点对象 -> 对象模型,下面将通过一个示例 HTML 代码与配图更形象地解释这个过程。

如下面的这段 HTML

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

DOM Rendering

# 优化工作清单

当小伙伴们出现了页面性能问题时,或者在设计我们的应用之前,就可以参考如下的条目进行快速检查。

  • 页面内容
    • 减少 HTTP 请求数
    • 减少 DNS 查询
    • 避免重定向
    • 缓存 Ajax 请求
    • 延迟加载
    • 预先加载
    • 减少 DOM 元素数量
    • 划分内容到不同域名
    • 尽量减少 iframe 使用
    • 避免 404 错误
  • 服务器
    • 使用 CDN
    • 添加 Expires 或 Cache-Control 响应头
    • 启用 Gzip
    • 配置 Etag
    • 尽早输出缓冲
    • Ajax 请求使用 GET 方法
    • 避免图片 src 为空
  • Cookie
    • 减少 Cookie 大小
    • 静态资源使用无 Cookie 域名
  • CSS
    • 把样式表放在 <head>
    • 不要使用 CSS 表达式
    • 使用 link 替代 @import
    • 不要使用 filter
  • JavaScript
    • 把脚本放在页面底部
    • 使用外部 JavaScript 和 CSS
    • 压缩 JavaScript 和 CSS
    • 移除重复脚本
    • 减少 DOM 操作
    • 使用高效的事件处理
  • 图片
    • 优化图片
    • 优化 CSS Sprite
    • 不要在 HTML 中缩放图片
    • 使用体积小、可缓存的 favicon.ico
  • 移动端
    • 保持单个文件小于 25 KB
    • 打包内容为分段(multipart)文档

# 优化关键渲染路径

假设有一个 HTML 页面,它只引入了一个 CSS 外部文件:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

它的关键渲染路径如下:

img

首先浏览器要先对服务器发送请求获得 HTML 文件,得到 HTML 文件后开始构建 DOM 树,在遇见 <link> 标签时浏览器需要向服务器再次发出请求来获得 CSS 文件,然后则是继续构建 DOM 树和 CSSOM 树,浏览器合并出渲染树,根据渲染树进行布局计算,执行绘制操作,页面渲染完成。

有以下几个用于描述关键渲染路径性能的词汇:

  • 关键资源:可能阻塞网页首次渲染的资源(上图中为 2 个, HTML 文件与外部 CSS 文件 style.css )。
  • 关键路径长度: 获取关键资源所需的往返次数或总时间(上图为 2 次或以上,一次获取 HTML 文件,一次获取 CSS 文件,这个次数基于 TCP 协议的最大拥塞窗口,一个文件不一定能在一次连接内传输完毕)。
  • 关键字节:所有关键资源文件大小的总和(上图为 9KB )。

接下来,案例代码的需求发生了变化,它新增了一个 JavaScript 文件。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js"></script>
  </body>
</html>

img

JavaScript 文件阻塞了 DOM 树的构建,并且在执行 JavaScript 脚本时还需要先等待构建 CSSOM 树,上图的关键渲染路径特性如下:

  • 关键资源: 3( HTMLstyle.cssapp.js
  • 关键路径长度: 2 或以上(浏览器会在一次连接中一起下载 style.cssapp.js
  • 关键字节:11KB

现在,我们要优化关键渲染路径,首先将 <script> 标签添加异步属性 async ,这样浏览器的 HTML 解析器就不会阻塞这个 JavaScript 文件了。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

img

  • 关键资源:2( app.js 为异步加载,不会成为阻塞渲染的资源)
  • 关键路径长度: 2 或以上
  • 关键字节: 9KB( app.js 不再是关键资源,所以没有算上它的大小)

接下来对 CSS 进行优化,比如添加上媒体查询。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" media="print" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

img

  • 关键资源:1( app.js 为异步加载, style.css 只有在打印时才会使用,所以只剩下 HTML 一个关键资源,也就是说当 DOM 树构建完毕,浏览器就会开始进行渲染)
  • 关键路径长度:1 或以上
  • 关键字节:5KB

优化关键渲染路径就是在对关键资源、关键路径长度和关键字节进行优化。关键资源越少,浏览器在渲染前的准备工作就越少;同样,关键路径长度和关键字节关系到浏览器下载资源的效率,它们越少,浏览器下载资源的速度就越快。

# 其他 JS 在线测试网站 

  • jsbench.me
  • jsben.ch
  • measurethat.net
  • webpagetest.org

# 性能优化之 35 条雅虎军规

  • 尽量减少 HTTP 请求个数 —— 须权衡
  • 使用 CDN(内容分发网络)
  • 为文件头指定 Expires 或 Cache-Control ,使内容具有缓存性
  • 避免空的 src 和 href
  • 使用 gzip 压缩内容
  • 把 CSS 放到顶部
  • 把 JS 放到底部
  • 避免使用 CSS 表达式
  • 将 CSS 和 JS 放到外部文件中
  • 减少 DNS 查找次数
  • 精简 CSS 和 JS
  • 避免跳转
  • 剔除重复的 JS 和 CSS
  • 配置 ETags
  • 使 AJAX 可缓存
  • 尽早刷新输出缓冲
  • 使用 GET 来完成 AJAX 请求
  • 延迟加载
  • 预加载
  • 减少 DOM 元素个数
  • 根据域名划分页面内容
  • 尽量减少 iframe 的个数
  • 避免 404
  • 减少 Cookie 的大小
  • 使用无 cookie 的域
  • 减少 DOM 访问
  • 开发智能事件处理程序 (事件代理)
  • 用 <link> 代替 @import
  • 避免使用滤镜
  • 优化图像
  • 优化 CSS Sprite
  • 不要在 HTML 中缩放图像 —— 须权衡
  • favicon.ico 要小而且可缓存
  • 保持单个内容小于 25K
  • 打包组件成复合文本

# 参考资料

  1. 浏览器的工作原理:新式网络浏览器幕后揭秘 强烈推荐
  2. 渲染树构建、布局及绘制
  3. 优化第一个字节的时间
  4. TCP 的三次握手与四次挥手(详解 + 动图)推荐
  5. 常见性能优化策略的总结
  6. Page Speed
  7. web 性能优化篇
  8. 2018 前端性能优化清单
  9. (译)2019 年前端性能优化清单 — 上篇
  10. (译)2019 年前端性能优化清单 — 中篇
  11. (译)2019 年前端性能优化清单 — 下篇
  12. PageSpeed Insights 规则
  13. 浏览器渲染过程与性能优化
  14. 如何提升 CSS 选择器性能
  15. 前端性能优化:2018 年 JavaScript 开销及优化工具和方法
  16. 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理推荐
  17. 彻底理解浏览器的缓存机制 推荐 2
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

dmq 微信支付

微信支付

dmq 支付宝

支付宝