Webnovel 不用照顾 Edge 浏览器性能?想多了!

avatar
前端工程师 @上海阅文信息技术有限公司

本文作者:任家乐

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。

前言

曾写过一篇性能优化 “ 长篇报告 ” 「 checkbox 美化引发的蝴蝶效应 」 ,也曾感叹 CSS 对渲染的影响是如此大,也许深化记忆点的代价就是被同一块石头绊倒2次 ?是的,性能优化“报告”第二弹来了,希望本篇文章可以在优化页面性能上给大家提供一些思路。

看点

某些 CSS 属性或 JS 操作,看似简单却易隐藏陷阱?

  • visibility - 在不改变文档布局的情况下展示或隐藏某个元素。
  • clientHeight / clientWidth - 获取元素内部的宽高。

磨刀不误砍柴工,性能工具的亲密接触总是必不可少的。

  • Edge 浏览器性能工具怎么用
  • 如何快速的定位、解决性能问题?

同是浏览器,渲染层面的表现却如此不同?

  • Edge 浏览器的渲染策略和 Chrome 有何不同?

性能炸弹

问题始于一个 “ 红色性能炸弹 ” ,瞄准的是 Webnovel「起点海外版」 PC 站点阅读页,关键字:Edge 浏览器、CPU、RAM。Webnovel 的国外用户指出,在 Edge 浏览器下,阅读页 CPU 消耗的非常大,他的腿甚至感受到了笔记本电脑的灼烧。

Webnovel 阅读页,为了更趋近于完美的阅读体验,我们做了:预加载上下章、支持上下键切换章节、无刷新跳章节等优化,在阅读过程中,你甚至不会看到内容加载过程中的 loading 。然而面对这次的性能突击,这些优化在 Edge 浏览器下显得如此渺小...而对于是否值得做此次性能优化,Google Analytics 则给出了答案:

「 Webnovel 浏览器占比一览」

Edge 浏览器 TOP4 (2.53%)的占比情况已不容忽略!看来注定免不了和 Edge 的一场针锋相对了,立马去 Edge 浏览器上一探究竟~

问题的剖析

定位问题之前,内心有个声音在提醒我:是 Google 广告脚本的问题!因为前不久阅读页引入了 Google 广告,并且针对广告的展示及隐藏做过一些逻辑的调整,带着目的首先从 JS 开始分析。

性能工具中寻找问题代码 - Google 脚本在不断获取 clientHeight、clientWidth?

我是用 Edge 浏览器自带的性能工具来进行分析的,由于 Edge 只能在 win10 系统上运行,我工作中用的是 iMac 所以几乎没有使用过 ,也只能回家 “踢走” 老公倒腾他专门打游戏的台式机了,其 Edge 浏览器版本:42.17134.1.0。

打开 Edge 开发者工具,性能工具的入口映入眼帘。

「 Edge 开发者工具 」

切入正题:「 访问 Webnovel 阅读页 - 打开“ 性能 ” 标签栏 - 按下运行按钮 」。为了还原用户的使用场景,我也认真的滚动浏览了十几章。运行的结果:

「 首次性能工具的结果图 」

这满屏的原谅色,看到这张图,答应我请第一眼就确认是 “渲染” 的问题好吗?请立马注释 CSS 一探究竟好吗?但我并没有做到。

继续观察,发现性能工具中有2个子标签栏: 时间线、JavaScript 调用堆栈。调用堆栈里很清楚的指明了 CPU 占用率最高的2项(下图),果然出自于 Google 广告的 osd.js 脚本。

「 首次性能工具结果图之 JavaScript 调用堆栈 」

不能慌,证据还不够确凿。Edge 的性能工具其实可以精确定位到 JS 中的问题代码片段,在红框区域点击鼠标右键,也可以定位到问题源码,这一点 very good!感觉离事实的真相更近了一步。

「 追溯代码问题细节图 」

「 问题代码片段1 」

「 问题代码片段2 」

显而易见,Google osd.js 脚本设置了计时器,不停地获取 HTML 节点的 clientWidth 和 clientHeight,所以祸根源于此?

clientHeight、clientWidth 的一些隐患

Facebook 的工程师 Stoyan 在他的博客中曾提过, 浏览器是聪明的,为了节约资源、不频繁触发 Repaint(重绘)、Reflow(重排),它会将一系列脚本中需要执行的 UI 改动放在一个队列中一起执行,而不是每个改动都单独执行从而引发无数次的 Repaint、Reflow。BUT!有一些操作会打乱它原有的 plan 和秩序:

  1. offsetWidth,offsetHeight,offsetTop,offsetLeft
  2. scrollTop, scrollLeft,scrollWidth, scrollHeight
  3. clientlTop, clientLeft,clientWidth, clientHeight
  4. getComputedStyle(),or currentStyle in IE

浏览器对于这些操作,会“ 非常及时地 ”给予最准确实时的答案,因此会触发 Reflow,例如重新计算样式、更新图层等。要知道 Reflow 相比 Repaint 对渲染的影响更大、损耗更多。

demo time - 获取 clientWidth 是否会触发不必要的渲染?

功能简述:

  1. 页面 onload 后设置计时器每隔 4ms(浏览器默认最小时间间隔) 获取 HTML 节点的clientWidth。
  2. 点击 click me 按钮水平位移 box1 到最右侧。
  3. 点击 no timer 按钮清除计时器,同时水平位移 box1 到原位。

「 demo 动图 」

过程

结果

阶段一:before “ click me ”

未触发 Reflow 等渲染事件

阶段二:after “ click me ”

触发了额外的渲染(recalculate style、update layers)

阶段三:after “ no timer ”

没有触发 Reflow 等渲染事件

Chrome Performance 工具则更直观地描述了表格中的结果(红框中处于阶段二,清楚表现了额外的渲染):

为了更突出差异,我将水平移动的 div 数量增加到了 20 个,Recalculate Style 的数据相差了 70ms:

「 渲染数据的差异:动画期间获取 clientWidth(上),动画期间不做任何操作(下) 」

demo 的结果验证了在 Chrome 浏览器中,动画期间获取 clientWidth / clientHeight 确实会引发浏览器的 Reflow,随着动画复杂度的增加以及元素的增加,该影响只会更大,进而增加了不必要的渲染消耗。

首次尝试 - 删除 Google 广告

既然阅读页 CPU 飙升极有可能是因为 Google 的 osd.js 不断获取 HTML 节点的 clientWidth / clientHeight 意外触发了多次 Reflow ,那么解决问题的第一步即是删除 Google 广告。我利用 fiddler 代理 JS 脚本到本地,删除了广告相关的代码。

但其结果尽和我的猜测完全不同,删除广告并没有使页面性能变得更好,有点没有头绪...真的不是 Google 广告的问题?

再次试探 - Google 广告在其他站点有无类似问题

继续用 Edge 性能工具对一类似网站进行测试,因为这个网站同样使用了 Google 广告,并且相比于 Webnovel 阅读页首次初始化1枚广告,它的首页尽然同时加载了 4 枚广告。然而结果却没有渲染上的问题(如下图),网站 CPU 利用率一栏甚至完全没有飚绿的痕迹。

推翻设想,转换方向到 CSS

太过坚信设想反而偏离正轨?前期我太坚信是 Google 广告脚本的问题,既然以上 2 次的尝试都证明不是 Google 广告脚本的锅,那么是时候转换方向了。

曾经写的「 checkbox 美化引发的蝴蝶效应 」带给了我一些灵感:CSS 在渲染性能上有着相比 JS 更不容忽视的影响力。结合前面提到的 demo 结论,不由得猜测也许阅读页的性能问题是由于某些 CSS 属性或动画与 Google 广告脚本中的 clientHeight、clientWidth 产生了相互作用,引发了额外的渲染。此时尝试注释所有样式,最终绿色竟然消失了,这说明极有可能是 CSS 的锅。

「 CPU 占用率中消失的渲染 」

罪魁祸首是 visibility : hidden ?

继续从性能工具中寻找更精确的答案,意外发现 Edge 性能工具里的 “ 时间线 ” 标签栏里可以捕捉到很多有用的信息,甚至可以找到样式相关的条目?展开即可定位到目前引发渲染的 DOM,这一点太赞了!

这对问题的定位有了很大的帮助,因为我很清楚地看到了有问题的 DOM ,也就是我们写的 loading 组件。

Webnovel 阅读页确实存在几枚隐藏的 loading,分别存在于章评弹窗中、目录弹窗中、各章节内容之间。

「 章评弹窗、目录弹窗 」

「 章节之间的 loading 」

隐藏 loading ,是为了简化逻辑,将其“ 埋伏隐藏 ” 在各自需要的地方,在获取所需的异步数据之前,可以第一时间将 loading 展示给用户,获取到需要的数据后再将 loading 隐藏,这样的交互逻辑是合理的,那么隐藏的 loading 为什么会对渲染产生影响?Webnovel 阅读页弹窗中的 loading 是用 visibility: hidden 来隐藏的,这样的方式不对?

visibility: hidden 有利有弊

看不见 loading 的踪迹,它却依然能对渲染产生这么大的影响?你认为正确地隐藏了其中的 loading,而 loading 却实际地存在于文档流中,它的每一个动画,都会影响同在文档流中的其他节点。

  • visibility: hidden // DOM 的渲染不会忽略其所有节点
  • display: none // DOM 的渲染忽略其所有节点

如此来看,性能更优的应该是 display: none,不在文档流中就不会影响其他的元素。但每个被创造出来的 CSS 属性必然有创造它的道理,如果 display: none 是最推荐的方式,为什么还会有 visibility : hidden 的存在?任何事物都存在两面性,例如以下场景中,visibility: hidden 竟协助某大神解决了性能问题。

Google 的工程师 Jake Archibald 在他的博客「Solving rendering performance puzzles」中,用一个有趣的 SVG 动画验证了 Layout (布局)在网页性能中举足轻重的地位。随着动画中 SVG 文字的不断变化,动画的帧率由原来最优的 60fps 最终降低到不足于 10fps。Chrome 的 Performance 工具揭示了其主要因素是产生了大量的 Layout 消耗(称为 Layout Thrashing or Reflow )。

此时 Jake 利用 visibility: hidden 解决了 Layout 消耗过多的问题,他提到,visibility: hidden 的元素,因为其在文档流中占据一席之地,因而将其设置为 visibility: visible 并不会产生 Layout 上的消耗,只会产生额外的 paint,借助此想法,他事先将 SVG 中的所有字符分别放置于 <textpath> 节点中的子节点 <tspan> 中,并利用 visibility: hidden 进行隐藏:

「 DOM 中隐藏的 SVG 文字 」

随后逐次将 <tspan> 的 visibility 属性值变更为 visible ,字符就依次展示了出来,对比下,他原来的做法就真的是非常粗暴了,即:将所有字符存于单独的一个节点中,每次截取固定长度的文字设置为 <textPath> 的值。大量的重置 textpath 节点值最终引发了浏览器每个字符的重新布局( layout )。

因此并不能笼统地下定论 visibility: hidden 不如、或优于 display: none ,只能说在不同的场景下他们都有各自的优势。

问题的修复

显然,loading 动画用 visibility: hidden 进行隐藏确实产生了很多额外的渲染消耗。修复这个问题的方案很多,例如,可以将 loading 的隐藏方式改为 display: none,或者是在打开弹窗的时候将 loading 实时添加到 DOM 中,最终为避免改动弹窗组件的 CSS 产生的全局影响,我们在打开弹窗的时候添加了 loading 的 DOM 节点,关闭弹窗时则将其移除。一周后用户的反馈也证明了此方案是有效的(下图)。

「 用户的反馈 」

新方案的尝试

Edge 浏览器渲染上和 Chrome 有什么不同 ?

Webnovel 阅读页的问题虽然得以解决,但可能我们都会感到疑惑,为什么这样的性能问题在 Chrome 浏览器上并没有发生,或者说表现不明显?这里我借助「 visibility: hidden 有利有弊」一节中提到的 Jake 的 SVG 动画做了一波测试。性能工具简单探测一下,发现未优化前的动画其性能和 Chrome 一样无法直视:

「 Edge 浏览器性能结果 」

优化后的动画其表现和 Chrome 也相差甚远,直观上的对比如动图所示,Edge 浏览器(第二张图)上的动画比 Chrome(第一张图) 缓慢许多。

「 上 Chrome, 下 Edge 」

Chrome Performance 工具中可以证明 Jake 的优化策略确实将 Layout 产生的消耗降低了很多,由原来的 TOP3 变为最后一位 :

「 优化前,优化后 」

但 Edge 性能工具的结果甚至比不上优化之前,渲染上未降低,并且出现了很多 JS 上的 CPU 冲击(图中橘色区域):

「 优化前 」

「 优化后 」

优化后渲染层面的开销甚至大于优化前的开销(在动画运行差不多时长的情况下):

「 优化前、后 UI线程数据 」

在此 demo 的表现上,Edge 浏览器对 SVG 动画的渲染情况与 Chrome 截然相反,更重要的是,Chrome 对于 Layout(布局)和 Paint (重绘) 在渲染性能方面的 “ 权衡 ”似乎和 Edge 是不一样的,降低大量的 Layout 开销确实可以使 Chrome 中的动画更为流畅,但这在 Edge 中却行不通,甚至表现相反。

解决问题的新思路

Chrome 和 Edge 浏览器在 SVG 动画上的表现差异提供了我一些新的思路,再次回到 Webnovel 阅读页,我们的页面中确实使用了数量不少的 SVG,我尝试着将 SVG 的 display: inline-block 改为 display: none 后,CPU 竟然迅速降了下来,性能上也得到了缓解(如下图),感到非常惊喜。

「 注释 SVG 样式之前 」

「 注释 SVG 样式之后 」

但目前性能问题已然解决,并且 Webnovel 全站都使用的了 SVG ,此时若全局替换掉 SVG 未免有些粗暴,加之 Jake 的 demo 还不足以说明过多的 SVG 影响了页面的性能,因此该方案并没有实施,但这确实可以作为 Webnovel 阅读页性能问题的另一个突破点,后续还需要花更多的时间来探索这其中的奥秘。

一些碎碎念

当然,问题的修复总是需要总结一下:

  • visibility: hidden 是把双刃剑。
  • clientWidth、clientHeight 等操作在动画过程中不建议使用。
  • 如果性能工具展示了大面积飙绿现象,请第一时间注释你的 CSS 再一探究竟
  • 无法定位问题的时候可以对比其他类似的网站得到启发,从而进一步定位
  • Edge 浏览器渲染层面和 Chrome 略有不同,这是值得重视的。

性能优化本就是积极尝试和敢于试错的过程,正所谓积跬步以至千里,点滴的积累和探索不是为了产出 “ 最佳性能优化方案 n 条 ” ,而是为了能更好地提炼定位问题的思路、扩展认知范围、同时形成一种对待性能瓶颈不惧怕的态度,希望我们都可以做到微笑面对性能瓶颈。


参考链接


查看更多分享,请关注阅文集团前端团队公众号: