这些前端性能优化的知识我从来不告诉别人(下)

3,694 阅读13分钟

前言

相关概念了解请移步 这些前端性能优化的知识我从来不告诉别人(上篇) 本文干货居多噢

引言

当遇见“你为性能优化做了哪些事情”,

  • 70% 的人上来就说减少合并资源、减少请求、数据缓存这些优化手段;

  • 15% 的人会提到需要在 DevTools 下先看看首屏时间,围绕首屏来优化;

  • 10%的人会提到需要接入一个性能平台来看看现状,诊断一下;

  • 而只有 5% 的同学会从前端性能体系来系统考虑性能优化。

你有答案不? 假如你是面试官,或者前端负责人,你会要哪一种人。

毫无疑问,我会想要那5%。本篇文章和上篇不一样,都是实际干货。但还是希望各位看官能先看看上篇,起码对性能体系能有个模糊的概念。

前端性能体系

举个栗子。某天你的老板的老板跨越层级给你call了电话,问:“我刚打开首页很慢,但后面打开就不会了。这是怎么回事?用户是不是也跟我一样遇到了这样的问题?”

你老实告诉我,你会如何回答老板的老板(例如CEO)? 为了不背很大的锅,你会不会告诉老板的老板,这是用户网络情况不好导致的?

那更大的问题就来了, 是不是过几天,或者不定时的就会出现这种情况?你能每次都找到老板能接受的理由吗?不,你不能。

首页打开缓慢,原因有很多。老板期待的是,前端能和服务端一样,通过查查日志就能定位问题所在。如果你只停留在猜测层面不去解决掉问题,你猜猜你会是什么下场?实际上,能做到这点的人并不多见。

所以,前端到底有没有这样类似服务端的工具呢?有,那就是性能监控平台。要不,干脆你来做一个性能监控平台吧。要求不多,平台能监控各个业务的性能指标以及在对应场景下的性能标准。当遇到性能问题,你能直接判断当前表示性能的指标数据意味着什么。然后平台会提示你,问题到底是出在前端还是服务端,抑或是网络层的问题。

那什么是前端性能体系? 你可以初步理解为一堆性能指标的集合,针对集合中的各种元素组合进行监控、预警,甚至报警。

那又要问了,性能指标是怎么来的?

从 URL 输入到页面加载整过程分析

当你在浏览器输入 URL 并回车后,浏览器为了把 URL 解析为 IP 地址,会向 DNS 服务器发起 DNS 查询来获取 IP 地址。接着浏览器通过 IP 地址找到目标服务器,发起 TCP 三次握手和 TLS 协商,从而建立起 TCP 连接。

在建立TCP连接后,浏览器发起 HTTP 请求,服务端响应接收到的请求。接着浏览器从响应结果中拿到数据,并进行解析和渲染,最后在用户面前就出现了一个网页。

所以,前端性能瓶颈点是不是就出现在这个过程中? 从时间角度看,这个过程是不是可以看作是三个阶段?要不再给你说说介绍下 Web 前端都有哪些性能瓶颈点

客户端发起请求阶段

用户在浏览器输入 URL,命中本地缓存则直接走缓存出页面。。如果没有命中缓存,会由 DNS 查询从域名服务器获取这个 IP 地址,接下来就是客户端通过 TCP 的三次握手和TLS协商向服务器发起 HTTP 请求建立TCP连接的过程。

诺,这个阶段 本地缓存DNS查询、HTTP 请求都能是性能瓶颈点。懂了伐?

  • 本地缓存 (这点不多说了,免得被部分看官说是“老四样” /手动捂脸)
  • DNS查询
    • 每进行一次 DNS 查询,都要经历从手机到移动/电信的信号塔,再到认证 DNS 服务器整个过程。关键是这个过程时间以及足够长了,是用户不能接受的长时间等待。

    • 要节省时间,可以让 DNS 查询走缓存,浏览器也提供了 DNS 预获取的接口。我们可以在打开浏览器或者 WebView 的同时就进行配置。这样真正发请求时,DNS 域名解析可以检查一下浏览器缓存,一旦缓存命中,就不需要去 DNS 服务器查询了。

  • HTTP 请求
    • HTTP/1.1 最大的瓶颈是串行的文件传输和同域名的连接数限制。 这里也不多赘述了,上篇有提到。

服务端处理请求阶段

  • 数据缓存
    • 借助 Service Worker 的数据接口缓存
    • 借助本地存储的接口缓存和CDNContent Delivery Network,内容分发网络)
    • 为什么数据缓存会成为性能瓶颈点呢?这是因为每请求一次数据接口,需要从客户端到后端服务器,再到更后端的数据存储层,一层一层返回数据,最后再给到客户端,耗时很长,如果能够减少一次这个请求,为首屏时间争取了宝贵的时间。
  • 重定向
    • 所谓重定向,是指网站资源(如表单,整个站点等)迁移到其他位置。用户在访问站点时,用户请求从一个页面转移到另外一个页面的过程。

    • 在服务端处理阶段,重定向分为三类(都会引起新的DNS查询,产生新的 HTTP 请求。)

      • 服务端发挥的302重定向,
      • META 标签实现的重定向和前端 Javasript
      • 通过window.location 实现的重定向。

客户端页面渲染阶段(拿到数据开始渲染页面)

  • 构建 DOM 树的瓶颈点 (等待后续Virtual DOM -> 真实DOM 的文章吧)
  • 布局中的瓶颈点
    • 例如上篇评论中有童鞋提到的雪碧图。这里留个小问题:首页适合用雪碧图吗?
  • js计算
    • 例如红包雨
    • 例如长列表

以上,介绍了前端领域我们能改变的瓶颈点。还有其他方面,我没有提到。

  • 比如,操作系统。 我曾在一个15寸的屏幕上做首屏秒开,当时记得是操作系统是Android 5.3 版本。多苦,你想想就明白了。
  • GPUGUI等。
  • 网络层和服务层
    • 预防阻塞
    • 负载均衡
    • 慢启动
  • 算法
    • 页面解析、渲染的算法
    • 标记化算法
    • 树构建算法
    • GC
    • ……

搭建一个性能监控平台?

现在市场并没有功能做得很全面的产品。即使有,目前也没有好的开源项目。但如果仅仅只是正对公司部分业务搭建一个简单的性能监控平台是没有那么难的。

准备一,做好数据埋点。

目的是为了能获取到一部分上报数据。例如页面在1小时内被访问的次数,页面出错的次数,哪些位置、入口点击数最高等等。那说下几种埋点方式吧。

  • 手动埋点(代码埋点)

    • 纯手动写代码,调用埋点SDK的函数。在需要埋点的业务逻辑功能位置调用接口上报埋点数据。目前市场上第三方数据统计服务商(例如百度)大都采用这种方案;

    • 手动埋点的技术本质是什么呢?它能获取到哪些内容?

      • 域名:document.domainURLdocument.URL
      • 页面标题:document.title
      • 分辨率:window.screen.height & window.screen.width
      • 颜色深度:window.screen.colorDepth
      • Referrer:document.referrer
      • 客户端语言:navigator.language
    • 埋点做法

      // 命令式埋点
      ()=>{
          // ... 这里是你的业务逻辑代码
          sendData(params);  //这里是发送你的埋点数据,params是你封装的埋点数据
       }
      // 声明式埋点
      <div data-spm-data="{name:'点击',enevt:'touch',agent: '...'}">Touch</div>
      
  • 可视化埋点

    • 解决了纯手动埋点的开发成本和更新成本,通过可视化工具快速配置采集节点(圈点),在前端自动解析配置,并根据配置上传埋点数据,比起手动埋点看起来更无痕,这里的配置数据可以设置过滤条件,避免针对所有元素(比如全埋点),可以在调用开启自动监控API时通过设置一些特征属性,来过滤不符合条件的元素,实现只针对某些元素进行自动上报数据的需求。(这不多赘述了,细聊内容很多。等以后吧。)
  • 无埋点 (wait...)

准备二,定义好性能指标以及量化指标数据

  • 指标量化场景
    • 弱网情况下
    • 无网络情况下
    • 机型
    • 网络短暂异常
    • ……
  • 指标
    • 页面白屏时间
    • FPS
    • 页面秒开率
    • 页面报错率
    • 页面卡顿次数
    • ……

开发吧。

你啥都搞清楚了,真的可以尝试开发一个简易版本的性能监控平台了。

骨架屏

为什么要单独拿出来说。因为,骨架屏可以做一个很重要的性能指标:渲染数据到页面的时间。

所以,你认为的骨架屏是什么?例如以下这个样子?

image.png

是的,它是骨架屏。

Q: 骨架屏仅仅只能做到如此吗? 答: 你错了。如果骨架屏出现的足够快,那为什么我不在骨架屏中加入其它元素呢?举个栗子。

image.png

如图,这张卡片内容有三部分组成: 商品图片、商品名称、立即购买按钮。假如你的骨架屏能预先拿到文字相关的数据呢? 你是不是可以考虑,把这些商品的名字当中骨架屏的一部分。

事实上,我在做组件骨架屏的时候就发现了这个问题。抛开获取数据的时间不谈,组件骨架屏渲染一个文本节点的时间几乎可以忽略不计。所以,问题就转移到了数据上。那假如你把数据本地缓存了一份?

再假设一下,假如图片骨架屏也能预先拿到图片数据呢? 那是不是也可以尝试去渲染图片呢?

这里要被问到了,那做了这些还算是骨架屏吗?那我们是不是可以对骨架屏有另一个认知:只要没有绑定JS事件的元素都可以看作是骨架。

人有美丑,为什么骨架屏不能有? 那么这里又会有人杠了。你这又是数据缓存又是懒加载的,骨架屏需要这些吗?

那么,什么是骨架屏。 骨架屏的本质是不是对你的组件自动生成一套骨架? 至于生成什么样的骨架,你管我?我有性能监测平台的好不啦? 平台告诉我,我的页面秒卡率大幅增加了好不啦?

所以,最重要一点,即使很多人认为前端性能优化一提到就是老四样,你依然可以玩出花来。毕竟,实践出真知。

快照

快照是一个很有意思的事情。例如天猫,你下单的时候会给你生成交易快照,后面的所有流程都以交易快照的数据为准,保证你的钱不会后期被多花或者少花。 比较价格这种东西,变一变很正常。

还有一种快照的解释。例如手机截图,把当前用户看到的页面内容生成一张图片保存到相册中。

那,这里我说一种。将你的首页做一份快照,备份到native缓存里,每次进来先加载那份快照。这份快照就是index.html文件,放native包的本地缓存里。你觉得浏览器直接访问你的index.html快嘛?

又得要杠了。这怎么有点像SSR? SSR理,服务端会给客户端发送一份包含html的模版字符串,可以没有样式,可以没有js。但快照有样式,就跟摆了张图片覆盖在首页一样。那你觉得是渲染一张足以覆盖首页的图片快,还是访问一份index.html快?

预渲染

先解释下预渲染的本质吧。提前准备好以下两点:

  • index.html + js + css
  • data (JSON)

再上张图吧。

图片来自于网络,如有侵权请联系删除图片来自于网络,如有侵权请联系删除

结合图片描述,再来认真解释一下。

预渲染是指在用户访问这个页面之前,完成页面渲染的准备。 简单点说就是在用户访问页面之前,我将页面内容存放在用户看不见的地方,待用户需要看见内容的时候再把已经渲染完毕的内容挪到能看见的位置。

稍微总结下。

  • 在用户正式访问页面之前,要将页面的内容渲染完毕。
  • 在用户正式访问的时候,只需直接加载已经渲染好的内容。

具体咋实现呢?

NSRNative side rendering,客户端渲染)

通过客户端(Native)进行页面结构拼接,进而实现页面渲染的处理技术。

需要离线包提供模板等资源(如 HTML、JS、CSS )。预加载提供数据,把页面作为数据经过模板函数变化后产生的结果,然后通过 v8 引擎在客户端渲染出来。

  • 预加载 提前将数据准备好。例如在用户访问之前就将接口缓存下来,调用接口后把得到的data也缓存下来。

NSR 是怎么实现的呢?

  1. 首先是模板和数据必须准备好,保证用户点击页面链接进入后,这个页面的所有资源是准备好的。至于咋做,可以看一下(上篇)
  • 离线化
  • 预加载
  • 缓存
  1. 因为页面是动态的, URL 是静态的,我们需要实现一种页面与模版的映射机制,一般为多对一。 这个机制有助于 Native 快速定位到用户所需的模版。

  2. Native 端实现本地渲染服务。(这点有点类似SSR,关于SSR详细内容等待后续文章)。

  3. The End. 搞完 NSR 之后,前端代码并不需要做什么改动,只需要把后置流程准备好就可以了。后置流程指的就是指将渲染好的页面后放置在可视区域之外。

小结一下

每个公司环境都不一样,每一种技术方案并不一定需要完全实现,如果实现一部分就能性能达标,那就没必要花更多的成本去做剩下的事情。 其实,webpack做的预渲染更好理解一些。 代码贴出来给大家看看,小伙伴也可以去上篇看更详细的。

Webpack 实现预渲染的代码示例如下:
// webpack.conf.js
var path = require('path')
var PrerenderSpaPlugin = require('prerender-spa-plugin')
module.exports = {
  // ...
  plugins: [
    new PrerenderSpaPlugin(
      // 编译后的html需要存放的路径
      path.join(__dirname, '../dist'),
      // 列出哪些路由需要预渲染
      [ '/', '/about', '/contact' ]
    )
  ]
}

未完待续

下篇文章应该会讲一下解决白屏以及卡顿的方案。敬请期待。