性能瓶颈点:从URL输入到页面加载过程分析
- DNS查询
- TCP三次握手和TLS协商,建立TCP连接
- 发起HTTP请求
- 服务端对请求进行响应
- 浏览器从响应结果中拿到数据,并进行解析和渲染
大致可以分为三个阶段:
- 客户端发起请求
- 服务端处理请求
- 客户端页面渲染
客户端请求阶段的瓶颈点
发起请求阶段是指:
- 本地缓存确认是否已经存在这个网站
- 如果没有,DNS查询,TCP三次握手和TLS协商,发起HTTP请求
在这个过程中,本地缓存、DNS查询、HTTP请求很容易成为影响前端性能的瓶颈点。
本地缓存
让静态资源加载更快
我们知道,本地缓存可以让静态资源加载更快,当客户端发起一个请求时,静态资源可以直接从客户端中获取,不需要再向服务器请求。而想要让本地缓存发挥这个作用,就需要先在服务器上进行配置。
-
本地缓存一般包括
- 强缓存
- 协商缓存
两种形式。
-
强缓存是指浏览器在加载资源时
- 根据请求头的
expires
和cache-control
判断是否命中客户端缓存 - 如果命中,则直接从缓存读取资源,不会发请求到服务器,反之还需要走完整的资源请求流程
- 根据请求头的
-
协商缓存是指
- 浏览器会先发送一个请求到服务器
- 通过
last-modified
和etag
验证资源是否命中客户端缓存。 - 如果命中,服务器会将这个请求返回,但不会返回这个资源的数据,依然是从缓存中读取资源。 如果没有命中,无论是资源过期或者没有相关资源,都需要向服务器发起请求,等待服务器返回这个资源。
DNS查询
每进行一次 DNS 查询,都要经历从手机到移动信号塔,再到认证 DNS 服务器的过程。这中间需要很长的时间。
想要节省时间,一个办法就是让 DNS 查询走缓存。幸好浏览器提供了 DNS 预获取的接口,我们可以在打开浏览器或者 WebView 的同时就进行配置
HTTP 请求
在 HTTP 请求阶段,最大的瓶颈点来源于请求阻塞。
所谓请求阻塞,就是浏览器为保证访问速度,会默认对同一域下的资源保持一定的连接数,请求过多就会进行阻塞。
那么,浏览器同域名的连接数限制是多少呢?一般是 6 个。如果当前请求数多于 6 个,只能 6 个并发,其余的得等最先的请求返回后,才能做下一次请求。
所以我们在项目之初,做一些域名规划很重要。我们可以先看看当前页面需要用到哪些域名,最关键的首屏中需要用到哪些域名,规划一下这些域名发送的顺序。
除了域名规划,为了解决同域名下的阻塞问题,还可以做域名散列,通过不同的域名,增加请求并行连接数。常见做法是,将静态服务器地址 pic.google.com,做成支持 pic0-5 的 6 个域名,每次请求时随机选一个域名地址进行请求。因为有 6 个域名同时可用,最多可以并行 36 个连接。当然,这个域名个数不是越多越好,太分散的话,又会涉及多域名之间无法缓存静态资源的问题。
服务端数据处理阶段的瓶颈点
服务端数据处理阶段,是指 WebServer 接收到请求后,从数据存储层取到数据,再返回给前端的过程。
这个过程中的瓶颈点,就在于
- 是否做了数据缓存处理
- 是否做了 Gip 压缩
- 是否有重定向
其中,Gzip 压缩是一种压缩技术,服务器端通过使用 Gzip 压缩传输到浏览器端的文本类资源(有别于图片等二进制资源)的大小可以变为原来的 1/3 左右
多数情况下,运维团队都会默认开启 Gzip 压缩
数据缓存
数据缓存分为两种:借助 Service Worker 的数据接口缓存、借助本地存储的接口缓存和CDN(Content Delivery Network,内容分发网络)
-
其中 Service Worker 是浏览器的一个高级属性,本质上是一个请求代理层,它存在的目的就是拦截和处理网络数据请求。如果没有 Service Worker,请求每次直接落到 WebServer 上,需要走一次后端数据存取链路的流程,这会延长页面打开时间。
-
借助本地存储的接口缓存是指,在一些对数据时效性要求不高的页面,第一次请求到数据后,程序将数据存储到本地存储(store 或者
localStorage
、甚至客户端本身的存储) -
所谓 CDN,它的基本思路是,通过在网络各处放置节点服务器,构造一个智能虚拟网络,将用户的请求导向离用户最近的服务节点上
重定向
在服务端处理阶段,重定向分为三类:
- 服务端返回的302重定向
- META 标签实现的重定向
- 前端 JavaScript 通过
window.location
实现的重定向。
它们都会引发新的 DNS 查询,导致新的 TCP 三次握手和 TLS 协商,以及产生新的 HTTP 请求
页面解析和渲染阶段的瓶颈点
在页面加载过程中,当前服务端对数据加工聚合处理后,客户端拿到数据,接下来就会进入解析和渲染阶段。
-
所谓解析,就是 HTML 解析器把页面内容转换为 DOM 树和 CSSOM 树的过程。
-
DOM 树描述了标签之间的层次和结构。HTML 解析器通过词法分析获得开始和结束标签,生成相应的节点和创建节点之间的父子关系结构,直到完成 DOM 树的创建。
-
而 CSSOM 树,即 CSS 对象模型。主要描述样式集的层次和结构,HTML 解析器遇到内联的 style 标签时,会触发 CSS 解析器对样式内容进行解析,与上面解析器解析 HTML 过程类似,CSS 解析器遍历其中每个规则,将 CSS 规则解析为浏览器可解析和处理的样式集合,最终结合浏览器里面的默认样式,汇总形成具有父子关系的 CSSOM 树。
-
-
解析完后就是渲染。主线程会计算 DOM 节点的最终样式,生成布局树。布局树会记录参与页面布局的节点和样式。完成布局后,紧跟着就是绘制。绘制就是把各个节点绘制到屏幕上的过程,绘制结果以层的方式保存。当文档中各个部分以不同的层绘制时,相互重叠时,就必须进行合成,以确保他们可以以正确的顺序绘制和展示。
以上就是解析和渲染阶段,这个阶段流程环节多,逻辑复杂,瓶颈点也多,比如,DOM 树构建过程,CSSOM 树生成阶段,重排和重绘过程等
构建 DOM 树的瓶颈点
解析器构建 DOM 树的过程中,有三点会严重影响前端性能。
-
一个是当 HTML 标签不满足 Web 语义化时,浏览器就需要更多时间去解析 DOM 标签的含义。特别是解析器对标签的容错,比如将
<br>
写成了</br>
,又或者表格嵌套不标准,标签层次结构复杂等。遇到这些情况时,浏览器会进行语法纠错。这就会导致页面总的解析和渲染阶段需要更长的时间,严重影响页面展示性能。 -
另一个是 DOM 节点的数量越多,构建 DOM 树的时间就会变长,进而延长解析时间,拖慢页面展示速度。
-
最后一个是文档中包含
<SCRIPT>
标签时的情况。因为无论是 DOM 或者 CSSOM 都可以被 JavaScript 所访问并修改,所以一旦在页面解析时遇到<SCRIPT>
标签,DOM 的构造过程就会暂停,等待服务器请求脚本。在脚本加载完成后,还要等取回所有的 CSS 及完成 CSSOM 之后才继续执行。这个过程中用户看到的是白色的屏幕。因此外部
<SCRIPT>
常被称为“解析”阶段的拦路虎。有时就因为解析过程中多了一个<SCRIPT>
,造成页面解析阶段从 200ms 变为 1s。所以,外部脚本的加载时机一定要确定好,能够延迟加载就选用延迟加载。另外,我们可以通过使用 defer 和 async,告诉浏览器在等待脚本下载期间不阻止解析过程,这样做可以明显提升性能。
布局中的瓶颈点
在布局阶段,浏览器会根据样式解析器给出的样式规则,来计算某个元素需要占据的空间大小和屏幕中的位置(比如电商详情页一张 banner图片的高度、宽度和位置),借助结算结果,来进行布局。而主线程布局时,使用的是流模型的布局方式。所谓流模型,就是像水流一样,需要从左到右,从上到下遍历一遍所有元素。
假设我们在页面渲染过程运行时修改了一个元素的属性,比如在电商的商品详情页加入一条广告数据。这时布局阶段受到了影响。浏览器必须检查所有其他区域的元素,然后自动重排页面,受到影响的元素需要重新绘制,最后还得合成,相当于整个渲染流程再来了一遍。
除此之外,因为浏览器每次布局计算都要作用于整个 DOM,如果元素量大,计算出所有元素的位置和尺寸会花很长的时间。所以布局阶段很容易成为性能瓶颈点。
还有其他方面,比如,偏硬件领域,像 GPU 绘图、操作系统 GUI 和 LCD 显示等瓶颈点;网络层和服务层,如拥塞预防、负载均衡和慢启动;还有一些页面解析和渲染的算法,如解析算法、标记化算法和树构建算法,等等
案例分析:移动端 M 站性能优化落地注意事项
-
通过本地缓存方案来减少客户端请求数,同时让运维同事对静态资源配置了合理的过期时间,实现静态资源的强缓存方案
-
对页面做了 DNS 预解析处理
-
对一些广告和帮帮页(WebIM 应用,用于用户的咨询使用)做了懒加载。
-
为了确保页面解析不会中断,对列表页的加载资源做了处理,也制定了 CSS 相关的规范,确保嵌套层次更少,让规则解析更简单。
-
原先列表页的筛选功能使用的是同步技术,每次完成筛选,整个页面都需要重新加载一次,这一次把它修改成了 SPA 单页面应用,每次筛选后,只请求数据接口即可,不用整个页面都请求。
列表页改单页面应用
这些页面都在搜索引擎和其他 App 做了广告投放,需要考虑 SEO, 也就是说,URL 要和原来的页面保持一致的同时,支持爬虫够抓取和解析。
调研过程中发现,与直接切换 VUE/React 技术栈相比,现有的列表页模板修改成单页面应用,风险上要小很多,所以最终采用的是列表页模板的单页面实现。
浏览器的 History API 方案刚好能够支持单页面应用和原页面URL保持一致。具体来说,通过其中的 pushState 修改 URL 和历史记录,能确保筛选后的 URL 和原来保持一致,通过它的回退等功能操作,也可以回到上一个页面。
对于SEO这块儿,为了确保爬虫能够抓取解析这个页面,额外提供了一套服务端模板,如果页面访问头是爬虫的话,就走服务端模板,反之如果是个人用户的话,就走单页面应用。
弱网下性能优化
-
因为弱网下一个请求都需要 2s,而原来的列表页数据接口请求大约26个请求,于是就尽可能合并请求,最后合并到了11个。
-
小图标采用 Base64 Encoding 的方式,内嵌于页面中,不用额外发网络请求来获取。
-
针对弱网(特别是移动网络),不自动加载图片,只显示占位图。
如何实现监控预警并进行问题诊断
问题诊断
诊断清单:
-
全量 vs 增量
以京东 App 列表页为例,首屏一般展示 4 条左右的商品数据,PC 页首屏展示 50 条商品数据,后端数据接口一般是同一套(无论是针对移动端还是 PC 端),这时 App 列表页请求后端接口,后端一次返回 50 条数据就不合适了。更好的做法是,分接口先拉取首屏所需的 4 条数据,然后在页面滚动或者下拉操作加载后续数据即可。
值得注意的是,为了保持体验流畅,有时候我们会多加载几条,比如开始加载 6 条,刚开始滚动到首屏结束时,第 5 条数据已经有了(此时请求第 7 条数据),不需要等后端服务器返回就可以展示。
-
同步 vs 异步
比如电商 App 列表页,一般需要先去拉取导航位置图片和链接信息,然后去拉取商品列表信息,这会延长加载时间。此时我们可以考虑同时拉取两个列表,中间有依赖关系的地方,集中到第一个接口中去获取,然后就可以并行去请求两个接口了。
-
实时 vs 缓存
如果是能够缓存的数据(如双 11 的榜单、页面中的 JS、CSS 资源),建议优先检查一下是否做了缓存处理
有关静态资源(如 JS、CSS文件)缓存处理,比较好的方法是,将每次渲染后的页面做缓存,页面打开时就展示,然后局部细微做更新
-
原片 vs 压缩
做一些无损或者有损压缩处理,以此提高加载速度,或者尽量不使用原始图片(如 png-24),优先使用 webp 等图片格式
优化手段:首屏秒开的 4 重保障
4 个方法——
- 懒加载
- 缓存
- 离线化
- 并行化
懒加载
懒加载是性能优化的前头兵。比如当我们打开一个页面,它的内容超过了浏览器的可视窗口大小,我们可以先加载前端的可视区域内容,剩下的内容等它进入可视区域后再按需加载。
如果首屏只需要几条数据,后端接口一次可以吐出50条数据,这会导致请求时间过长,首屏特别慢。这种情况就非常适合用懒加载方案去解决。
具体怎么做呢?我们可以先根据手机的可视窗口,估算需要多少条数据,比如京东 App 列表页是 4 条数据,这时候,先从后端拉取 4 条数据进行展现,然后超出首屏的内容,可以在页面下拉或者滚动时再发起加载。
那么如果首页当中图片比较多,比如搜索引擎产品的首页,如何保证首屏秒开呢?同样也可以采用懒加载。以百度图片列表页为例,可视区域范围内的图片先请求加载,一般会根据不同手机机型估算一个最大数据,比如 ihone12 Pro 屏幕比较大, 4 行 8 条数据,我们就先请求 8 条数据,用来在可视区域展示,其他位置采用占位符填充,在滑动到目标区域位置后,才使用真实的图片填充。
缓存
如果说懒加载本质是提供首屏后请求非关键内容的能力,那么缓存则是赋予二次访问不需要重复请求的能力。在首屏优化方案中,接口缓存和静态资源缓存起到中流砥柱的作用。
接口缓存
接口缓存的实现,如果是端内的话,所有请求都走 Native 请求,以此来实现接口缓存。为什么要这么做呢?
App 中的页面展现有两种形式,使用 Native 开发的页面展现和使用 H5 开发的页面展现。如果统一使用 Native 做请求的话,已经请求过的数据接口,就不用请求了。而如果使用 H5 请求数据,必须等 WebView 初始化之后才能请求(也就是串行请求),而 Native 请求时,可以在 WebView 初始化之前就开始请求数据(也就是并行请求),这样能有效节省时间。
静态资源缓存
数据接口的请求一般来说较少,只有几个,而静态资源(如 JS、CSS、图片和字体等)的请求就太多了。以京东首页为例,177 个请求中除了 1 个文档和 1 个数据接口外,其余都是静态资源请求。
那么,如何做静态缓存方案呢?这里有两种情况,一种是静态资源长期不需要修改,还有一种是静态资源修改频繁的
-
资源长期不变的话,比如 1 年都不怎么变化,我们可以使用强缓存,如
Cache-Control
来实现。具体来说可以通过设置Cache-Control:max-age=31536000
,来让浏览器在一年内直接使用本地缓存文件,而不是向服务端发出请求。 -
资源本身随时会发生改动的,可以通过设置 Etag 实现协商缓存。具体来说,在初次请求资源时,设置 Etag(比如使用资源的 md5 作为 Etag),并且返回 200 的状态码,之后请求时带上 If-none-match 字段,来询问服务器当前版本是否可用。如果服务端数据没有变化,会返回一个 304 的状态码给客户端
离线化
离线化是指线上实时变动的资源数据静态化到本地,访问时走的是本地文件的方案。说到这里,你是不是想到了离线包?离线包是离线化的一种方案,是将静态资源存储到 App 本地的方案,不过,在这里,重点讲的是离线化的另一个方案——把页面内容静态化到本地。
离线化一般适合首页或者列表页等不需要登录页面的场景,同时能够支持 SEO 功能。那么,如何实现离线化呢?其实,打包构建时预渲染页面,前端请求落到 index.html 上时,已经是渲染过的内容。此时,可以通过 Webpack 的 prerender-spa-plugin 来实现预渲染,进而实现离线化
并行化
懒加载、缓存和离线化都是在请求本身上下功夫,想尽办法减少请求或者推迟请求,并行化则是在请求通道上功夫,解决请求阻塞问题,进而减少首屏时间。 我们在处理请求阻塞时,也可以加大请求通道数量——借助于HTTP 2.0 的多路复用方案来解决。
HTTP 1.1 时代,有两个性能瓶颈点,串行的文件传输和同域名的连接数限制(6个),到了HTTP 2.0 时代,因为提供了多路复用的功能,传输数据不再使用文本传输(文本传输必须按顺序传输,否则接收端不知道字符的顺序),而是采用二进制数据帧和流的方式进行传输。
其中,帧是数据接收的最小单位,流是连接中的一个虚拟通道,它可以承载双向信息。每个流都会有一个唯一的整数 ID 对数据顺序进行标识,这样接收端收到数据后,可以按照顺序对数据进行合并,不会出现顺序出错的情况。所以,在使用流的情况下,不论多少个资源请求,只要建立一个连接即可。
文件传输环节问题解决后,同域名连接数限制问题怎么解决呢?以 Nginx 服务器为例,原先因为每个域名有 6 个连接数限制,最大并发就是 100 个请求,采用 HTTP 2.0 之后,现在则可以做到 600,提升了 6倍。
你一定会问,这不是运维侧要做的事情吗,我们前端开发需要做什么?我们要改变静态文件合并(JS、CSS、图片文件)和静态资源服务器做域名散列这两种开发方式。
具体来说,使用 HTTP 2.0 多路复用之后,单个文件可以单独上线,不需要再做 JS 文件合并了。因为原先遇到由 A 和 B 组成的 C 文件,其中 A 文件稍微有点修改,整个C 文件就需要重新加载的情况,如今由于没有同域名连接数限制了,也就不需要了。
此外,为了解决静态域名阻塞,提升请求并行能力,需要将静态域名分为 pic0-pic5。虽然通过静态资源域名散列的办法解决了问题,但这样做的话,DNS 解析时间会变长很多,同时还需要额外的服务器来满足,如今,采用 HTTP 2.0 多路复用之后,也不需要这样做了。
在实际工作当中,前端工程师还会用到离线包和 SSR。但 SSR 的实现比较重,要做的改造比较多,要求开发者对 node 生态有很好理解和把握,而离线包依赖于 App 端内的环境,对于端外和 PC 站不具有普适性
优化手段:白屏 300ms 和界面流畅优化技巧
所谓白屏时间,一般是当用户打开一个页面,从开始等待到页面第一个字符出现的时间,白屏时间越短,给人感觉 App 速度快,体验好,能有效降低跳出率
白屏优化
基于影响白屏时间长短的两个主要因素来解决——
- DNS 查询
- 首字符展示
DNS 查询优化
-
前端侧,可以通过在页面中加入
dns-prefetch
,在静态资源请求之前对域名进行解析,从而减少用户进入页面的等待时间。如下所示:<meta http-equiv="x-dns-prefetch-control" content="on" /> <link rel="dns-prefetch" href="https://s.google.com/" >
其中第一行中的
x-dns-prefetch-control
表示开启 DNS 预解析功能,第二行dns-prefetch
表示强制对 s.google.com 的域名做预解析。这样在 s.google.com 的资源请求开始前,DNS 解析完成,后续请求就不需要重复做解析了。它可以为你减少 150ms 左右的 DNS 解析时间。 -
以上是一个轻量级的方案,通过它可以将 DNS 解析时间控制在 400ms以内(这个算是比较快的)。如果你想要将耗时进一步压缩,比如控制在 200ms,此时就需要一个重量级的方案了。具体来说,可以采用 IP 直连方式,原来是请求www.google.com,现在我们通过调用 SDK 进行域名解析,拿到对应的 IP(如 6.6.6.6),然后直接请求这个 IP 地址拿到数据。
当然,这个实现起来需要避过许多坑,比如,HTTPS 证书和配置文件。
-
Https 证书是指当客户端使用 IP 直连时,请求 URL 中的 host 会被替换成对应的 IP,所以在证书验证时,会出现 domain 不匹配的情况,导致 SSL/TLS 握手不成功。
怎么解决呢?在非 SNI(Server Name Indication,表示单 IP多域名)的场景下,可以把证书验证环节独立出来 (如 Hook证书校验环节),然后将 IP 替换为原来的域名。在 SNI 场景下,可以定制 SSLSocketFactory,在 createSocket 时替换为 IP,并进行 SNI/HostNameVerify 配置。
-
而配置文件方面,一般在域名只有两三个的情况时,我们可以用到它来做 IP 和域名的映射。但随着机房的扩大,每次扩机器都要升级配置文件,后续会非常麻烦。
对此我们可以采用 httpDNS 来解决。这是因为 httpDNS 可以准确调度到对应区域的服务器 IP 地址给用户,同时还可以避免运行商 DNS 劫持。具体来说,SDK 会通过发报文(类似系统向 DNS 运营商发的报文)向 httpDNS 做一个 HTTP 请求(也是通过 IP 直接请求),请求通过后拿到对应域名,然后进行 IP 直连,完成资源或者数据接口请求。
-
首字符展示优化
所谓首字符展示,通常我们会在页面加载过程中出现一个 loading 图,用来告诉用户页面内容需要加载,请耐心等待。但这样一个 loading 图既无法让用户感受到页面加载到什么程度,也无法给用户视觉上一个焦点,让人们的注意力集中在上面。
如何解决这个问题呢?我们可以使用骨架屏。骨架屏并没有真正减少白屏时间,但是给了用户一个心理预期,让他可以感受到页面上大致有什么内容。
那么,如何构建骨架屏呢?因为考虑到每次视觉修改或者功能迭代,骨架屏都要配合修改,建议采用自动化方案,而不是手动骨架屏方案(也就是自己编写骨架屏代码)。骨架屏的实现方法有以下三个步骤。
-
步骤一,确定生成规则,遍历所有的 DOM 元素。针对特定区块(如视频、音频)生成相应的代码块,获取原始页面中 DOM 节点的宽度、高度和距离视窗的位置,计算出当前设备快高对应的大小,转换成相应的百分比,然后来适配不同的设备。
-
步骤二,基于上述规则结合 CLI 工具可以通过脚手架自动生成骨架屏
-
步骤三,将骨架屏自动化注入页面,再利用 Puppeteer 把骨架屏代码注入页面中自动运行。整个过程比较复杂,且有不少坑。
卡顿治理
首先也还是问题的定位,先通过 charles 等工具抓包看一下数据接口,
- 如果是和数据相关的问题,找后端同事,或者用数据缓存的方式解决。
-
如果问题出在前端,一般和以下两种情形有关:
- 浏览器的主线程与合成线程调度不合理
- 以及计算耗时操作
浏览器的主线程与合成线程调度不合理
比如,在某电商 App 页面点击抽奖活动时,遇到一个红包移动的效果,在红包位置变化时,页面展现时特别卡,这就是主线程和合成线程调度的问题。怎么解决呢?
一般来说,
- 主线程主要负责运行 JavaScript,计算 CSS 样式,元素布局,然后交给合成线程,
- 合成线程主要负责绘制
当使用 height
、width
、margin
、padding
等作为 transition 值时,会让主线程压力很大。此时我们可以使用 transform
来代替直接设置 margin 等操作。
比如红包元素从 margin-left:-10px
渲染到 margin-left:0
,主线程需要计算样式 margin-left:-9px
,margin-left:-8px
,一直到 margin-left:0
,每一次主线程计算样式后,合成线程都需要绘制到 GPU 再渲染到屏幕上,来来回回需要进行 10 次主线程渲染,10 次合成线程渲染,这给浏览器造成很大压力,从而出现卡顿。
如何解决呢?我们可以利用 transform
来做,比如 tranform:translate(-10px,0)
到 transform:translate(0,0)
,主线程只需要进行一次tranform:translate(-10px,0)
到 transform:translate(0,0)
,然后合成线程去一次将 -10px 转换到 0px。这样的话,总计 11 次计算,可以减少 9 步操作,假设一次 10ms,将减少 90ms。
计算耗时操作
遇到这类问题,一般有两种解法:空间换时间和时间换空间。
-
空间换时间方面,比如你需要频繁增加删除很多 DOM 元素,这时候一定会很卡,在对 DOM 元素增删的过程中最好先在 DocumentFragment(DOM文档碎片)上操作,而不是直接在DOM上操作。只在最后一步操作完成后,将所有 DocumentFragment 的变动更新到DOM上,从而解决频繁更新 DOM 带来的卡顿问题。
-
至于时间换空间,一般是通过将一个复杂的操作细分成一个队列,然后通过多次操作解决复杂操作的问题。
比如 2010 年时,一个 WebIM 离线消息的项目,其中一个功能是将批量消息下载到本地,点击下载后,由于文件过大,浏览器出现卡顿甚至卡死的情况。怎么办?最后通过实现一个 Chunk 方法,创建了一个队列,定时取出队列的任务,然后在本地服务器上将内容再次合并的方式来解决。代码示例如下:
function chunk(array, process, context) { setTimeout(function () { //shift方法可以获取队列中下一个要处理的项目 var item = array.shift(); //通过call调用的process函数,这样可以设置一个合适的执行环境(如果必须) process.call(context, item); if (array.length > 0) { setTimeout(arguments.callee, 100); } }, 100); }
具体来说,是通过
array.shift
将数组切分为不同的队列任务,调用setTimeout
方法设置 100ms 的延时,最后将调用process.call
方法去执行,从而解决了复杂任务带来的卡顿问题。在实际使用时,尤其是做动画时候要注意,Settimeout 设置定时时间不准(比如设置 0ms 延迟,实际中会延迟 4ms 左右),如果时间间隔过小(如小于 16ms),还会出现因为掉帧导致的卡顿,所幸新版本浏览器(IE10+)提供了
requestAnimationFrame
方法,我们可以封装一个兼容性方法较好解决它。
优化方案的选择
很多同学会因为缺乏对节奏的把握,影响优化结果,比如明明时间很紧,要快点拿到结果,你却选择了一个长期的方案,导致迟迟看不到结果,项目被 cancel 掉。
一般来说,业务报出性能问题时,比如手机首页访问慢的问题,需要一个短平快的解决方案(比如一周内需要完成上线),快速上线。
因为时间的原因,这个性能优化方案过程中肯定是要有取舍的,比如在最终效果方面,性能指标定到首屏平均 1s 就可以了,更多是短期方案,也就是前端工程师可以独立完成的优化手段,如懒加载、离线化、异步化、骨架屏和缓存等手段
//End of Article