前端性能优化小结

本文来源于平时工作的一些总结,在写的过程中也看了很多资料,集结成文章以加深记忆。

1. 网络

1.1. 减少网络请求数量,压缩资源

压缩合并js、css、image、以及其他静态资源等等,这些都可以通过打包工具做到。例如webpack,已经发展到3.0而且有非常完整的中文翻译,基本上按需求配一套完整的(虽然配置一套会非常麻烦),就可以满足需求。Webpack可以打包输出多个js bundle,甚至提取出js的公共部分。这部分要根据需求自己调整。
对于使用框架的应用,使用脚手架会更加方便,例如:
GitHub - vuejs/vue-cli: Simple CLI for scaffolding Vue.js projects
GitHub - facebookincubator/create-react-app: Create React apps with no build configuration.

1.2. 使用CDN

CDN的全称是Content Delivery Network,即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。
——百度百科

总之,优点是让全球的用户访问都保证速度,缺点是有缓存。

1.3. HTTP缓存头

缓存策略
目前广泛采用的正是上图的策略,既能够灵活的更新css和js,以应对需要紧急fix的bug。另外一方面,通过Cache-ControlETag灵活控制静态资源的过期时间、更新策略等等。

推荐阅读:
HTTP 缓存 | Web | Google Developers:上图图片的出处
HTTP 缓存 - HTTP | MDN

1.4 HTTP 2.0

http 2.0 能够让资源并行加载,可以把第一点中的合并相关的方法都打回原形。//不过我并没有用过

推荐阅读:
HTTP2.0的奇妙日常 | AlloyTeam :这篇文章写得生动有趣,非常推荐一读。


2. 渲染

2.1. 老生常谈的渲染顺序

css在顶部(head),js在下(body的最后)。这是为了不让渲染被阻塞。

2.2. DOM相关

  1. 尽量减少DOM层级,能不嵌套就不嵌套
  2. 减少操作DOM,避免重绘(repaint)和重排(Reflow)。
    重排:即页面元素的布局改变(必定导致重绘);
    重绘:即页面元素重新被渲染;

现代浏览器已经优化了repaint和reflow,在连续操作的情况下,浏览器会将这些操作积累以后一起执行。但是写代码的时候还是要注意:

  1. 样式的修改通过类名class,而非ele.style.xxx

    1
    2
    3
    4
    5
    6
    7
    8
    // 注意:classList有一个自带toggle方法,不过仍然存在兼容性问题
    function toggleClass(ele, className){
    if(ele.classList.contains(className)){
    ele.classList.remove(className);
    }else{
    ele.classList.add(className);
    }
    }
  2. 不要在循环语句里查找dom,需提前缓存好。

  3. DOM节点生成 & 插入的正确姿势:
    使用createDocumentFragment()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let container = document.getElementById("container");
    let ele;
    let fragment = document.createDocumentFragment();
    data.map(item => {
    ele = document.createElement("span");
    ele.innerText = item;
    fragment.appendChild(ele);
    })
    container.appendChild(fragment);

以及我更加喜欢的innerHTML

1
2
3
4
5
6
7
8
let container = document.getElementById("container");
let str = '';
let ele = document.createElement('ul');
data.map(item => {
str += `<li class="item">${item}</li>`;
})
ele.innerHTML = str;
container.appendChild(fragment);

2.3. 动画

js动画和css动画的性能之争是老生常谈的故事了。一般来说,能用css写的动画,我都会用css写。
引用google的总结——

当您为 UI 元素采用较小的独立状态时,使用 CSS。 CSS 变换和动画非常适合于从侧面引入导航菜单,或显示工具提示。最后,可以使用 JavaScript 来控制状态,但动画本身是采用 CSS。
在需要对动画进行大量控制时,使用 JavaScript。 Web Animations API 是一个基于标准的方法,现已在 Chrome 和 Opera 中提供。该方法可提供实际对象,非常适合复杂的对象导向型应用。在需要停止、暂停、减速或倒退时,JavaScript 也非常有用。

  1. js 帧动画:
    动画时需要保持60fps的帧率才能说服人眼,js写动画一般有如下两种方式:
  • setTimeoutsetInterval手动设置帧率(16ms,即1000s / 60)。但是由于机器真正的刷新频率不同,刷新频率与重绘频率不一致导致性能浪费,以及在离开浏览器tab时,动画并不会停下,导致CPU和电池持续消耗。
  • 使用requestAnimationFramerequestAnimationFrame可以根据机器的刷新频率让浏览器在真正的下一帧执行动画,从而达到流畅的动画效果。相当于将计算帧率的事情交给浏览器,而浏览器还能进一步为你优化这个计算。在离开浏览器tab时,动画会停止,节约CPU和电池资源。

两种渲染方式对比:requestAnimationFrame vs setTimeout - JSFiddle
查看兼容:Can I use

  1. css动画:
    涉及到修改元素尺寸或者位置时,减少使用盒模型相关的:margin、padding、width、height,以及定位属性相关的:top、right、bottom、left等等,上述属性都是常用的动画相关的属性,但是会触发重排,引起性能问题。
    尽量使用transform属性,例如其中的translatescalerotate等等。同时可以强行加上3d开启硬件渲染。
    除此之外,opacity也不会触发重排。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @keyframe moveToRight{
    from {
    opacity: 1,
    // bad
    margin-right: 0,
    // good
    transform: translate3d(0, 0, 0);
    }
    to {
    opacity: 0,
    // bad
    margin-right: 10px,
    // good
    transform: translate3d(10px, 0, 0);
    }
    }

3. js语法

这里能说的很多,大牛们甚至能通过编译器深入探讨js执行的性能问题。
能力有限,这里仅仅举几个常用的例子:

  1. 循环时提前缓存数组长度——计算的结果若需要多次使用,则先进行缓存。

    1
    2
    3
    for(let i = 0, len = data.length; i < len; i++){ 
    // do something
    }
  2. 使用switch...case代替if...else。除去性能问题,个人也更加喜欢前者,看着更加清爽干净。

  3. 使用===而非==,免于隐式转换。
  4. 数组的遍历,数据的遍历方式多种多样,包括for-in、for-of、map、for、while等等。for-in由于性能问题备受诟病,最原始的for和while反而是性能最突出的。//虽然我还是最喜欢map
    …等等。

推荐阅读:
吹毛求疵的追求优雅高性能JavaScript · Issue #2 · jawil/blog · GitHub

4. 其他

4.1. 一些技巧

  1. 懒加载以及图片的N种兜底图方法。
    我们通常会给图片留下一定的空间,避免内容加载时页面布局不停的抖动。
    懒加载的基本原理是,不将所有的图片同时加载出来,而是监听页面滑动,滑动到当前或即将进入屏幕的图片才会被加载出来。
    兜底图就是真正的图片还未加载出来时显示的图片,与图片懒加载技术相辅相成。如果说懒加载是为了优化性能,那么兜底图就是为了优化体验。图片兜底图的样式则是多种多样,例如:
  • 用同一张静态的图片兜底,一般是网站的标识图
  • 用纯色,这两种方法是最简单而且最单调的,因为所有图片都是一样的,但是对于非图片为主体的页面来说,其实是足够的。
  • 压缩出一张分辨率较低的图,手动实现渐进式图片的效果。
  • 通过服务器或者压缩后的小图,提取图片主要颜色呈现各种效果,例如纯色、渐变色等等
  • 通过一些算法,用原图直接生成一些体积非常小,但是有趣的图,例如黑白的、描边的、抽象的等等。

推荐阅读:
使用渐进式 JPEG 来提升用户体验 - 简书
How to use SVG as a Placeholder, and Other Image Loading Techniques:这个作者介绍了一种很棒的兜底图,看上去很高科技很酷炫,有空想要翻译或者使用一下。

  1. 节流和去抖动
    在滑动或者用户输入这些频繁被触发的事件里,我们应该尽量减少复杂的操作、减少复杂的计算,否则可能导致用户输入卡顿、或者资源的浪费。
    节流(throttle),也就是定时触发某个任务。
    去抖动(debounce),任务执行完以后,甚至执行完的一段时间后才能再次触发任务。一般都是使用这个。

推荐阅读:
深入浅出throttle和debounce · Issue #17 · stephenLYao/stephenLYao.github.io · GitHub

4.2. AMP(Accelerated Mobile Pages)

AMP是google出品的,能加速页面显示和加载。跟PWA一样,也是google不遗余力推的一个技术(所以中文文档也是相当全面且规范)。然而AMP实现性能优化的方法,是通过限制开发者随心所欲使用js和css实现的,因为官方已经用最优方法帮你实现了一些东西,你只要去使用即可,几乎不需要自己去造轮子。
一方面,要使用AMP定制的标签、组件(有点类似于react组件,但是不需要自定义,只需要使用其暴露出来的方法即可),来组装页面的各个部分。现在组件已经比较多了,可以满足很多需求。
另一方面,开发者不能插入自己的js,不能使用外联css,还有其他各种限制,颇有种带着镣铐跳舞的感觉。
也正是因为如此,AMP所能使用的范围终究是有限的。例如新闻、文档等内容展示等等,目前官方支持的一些组件其实也是往这方向发展的,例如广告组件、社交分享组件、页面分析组件、多媒体组件等等,也许跟ssr是绝配(虽然没有用过)。官方也有电子商务网站的示例,但是比较终究是比较简单的。另外,需要跟native进行交互的页面也自然无法使用。
将页面改用AMP实现并不单单是开发者的事,除非你同时也是设计师和PM。正确的流程是,重新审视页面的需求、所需要的技术。然后权衡是否要为了性能,重新设计页面的交互,甚至放弃页面中的一些东西。这其中,不仅仅是开发者需要反复翻看文档,甚至PM和设计师也需要。

4.3. WebAssembly

能够直接解决js的性能瓶颈,仅仅看文档了解了一下。以后有时间再看看。