关于前端性能优化,其实网上也有很多文章已经讲了很多,但是随着 webpack 4, babel 7 等工具的发布以及一些前沿技术的挖掘,很多以前的优化手段,例如雅虎34军规等已经不足以满足我们的需求了。刚好最近项目做了很多优化工作,所以来跟大家一起分享一下前端性能优化这个话题,以及个人做的一些优化工作。
首先,要知道如何优化一个网站,那么就得清楚,从输入网址,到看到页面视图,这个过程到底发生了什么。
那么,我大概将这个过程分为三大模块,分别是:
接下来我们将从这三方面探索前端优化技术
网络
首先,输入网址,通过 chrome devtools 的 timeline 板块 我们可以看到这个过程到底发生了什么:
请求 timeline 图要清楚怎么优化,那么就得知道,这些过程代表什么,其瓶颈又是什么。
这些耗时数据同时也可以通过 Performance API 来捕获到,用以分析 web 应用瓶颈。
Performance 时间节点请求阻塞 (stalled)
根据HTTP1.0/1.1协议规定,一个域名的并发请求量存在限制 :
一般情况下,我们的 web 应用有可能会有多个资源,一旦请求资源过多,请求就会被阻塞掉。导致耗时长,影响用户体验。
减少资源请求量
多域名(CDN)
一个域名存在请求量限制,为什么不把资源放在多个域名下呢? 比如 github 上就是用了 avatars.githubusercontent.com 等域名 存放头像图片资源,一定程度上提升了响应速度和用户体验。
缺点:多域名,每个新域名都需要重新进行DNS解析(只需解析一次),DNS的解析时间会变长。
HTTP/2(SPDY)
Multiplexed support(one single TCP connection for all requests) : 多路复用
在同一个 TCP 连接之中并行执行多个请求,不再有 浏览器请求并发限制。
DNS 解析
我们知道,当我们访问一个网站如 www.amazon.com 时,需要将这个域名先转化为对应的 IP 地址,这是一个非常耗时的过程,当然浏览器有DNS缓存,一旦访问过,再次访问从缓存中读取就会快很多。
一个从未访问过的域名解析耗时长达 1 秒DNS prefetch
要优化 DNS 解析时间,我们用到一种 DNS prefetch 的技术。
DNS prefetch 会分析这个页面需要的资源所在的域名,浏览器空闲时提前将这些域名转化为 IP 地址,真正请求资源时就避免了上述这个过程的时间。
<meta http-equiv='x-dns-prefetch-control' content='on'>
<link rel='dns-prefetch' href='http://g-ecx.images-amazon.com'>
<link rel='dns-prefetch' href='http://z-ecx.images-amazon.com'>
当我们的资源存放在不同的域名下,那么提前声明好域名,就可以节省域名解析的时间
TTFB (time to first byte)
用户拿到资源的耗时, 一般来说这个性能瓶颈是后端负责的。一般优化方法有,异地机房,CDN,提高带宽,提高 CPU 运算速度 等方式来来提高用户体验
CDN
CDN(内容分发网络),其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。
通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
CDN目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,解决 Internet 网络拥塞状况,提高用户访问网站的响应速度。
资源
通过了一系列的网络请求过程,资源到了 Content Download 的过程。
资源下载的耗时 = 资源大小 / 用户网速。
用户网速我们无法控制,那么资源的瓶颈其实很显而易见:资源大小。
减少资源文件大小,就能降低资源下载耗时。
在此进行资源优化之前,我们先将web 应用的资源文件按类型分为以下三种:
我们就分别从这几个方面了来说下资源优化方案。
常规优化
首先是常规优化操作,即对上述所有资源都通用的优化方法。
打包压缩
缓存
强缓存
体现为 from disk/memory cache, 具体是 from disk cache 还是 from memory cache 由浏览器自身控制
协商缓存
协商缓存由服务端控制,体现为 304 not Modified
缓存策略流程图
具体缓存策略流程图如下:
第三方库
开源的第三方库,如 vue , react, 之类的,一般是指 package.json -> dependencies 的包。
抽离第三方库
第三方库一般比较稳定,一般比较很少会变更,而业务代码可能会频繁变更。
所以,如果不抽离打包,业务变动后,用户需要重新下载全部的代码:
修改代码部署后需要重新下载100KB而抽离第三方库进行打包则只需要下载变更的业务代码
修改代码部署只需要重新下载20KBtree shaking
tree shaking 是基于 es modules 的静态结构 筛除没有用到的代码(dead code)
注意事项:
polyfill
自 ES2015/ES6 发布以来,现在已经到了 ES2018 了,但是依然有许多老旧浏览器依然占有一定的市场份额,所以我们依然需要对这部分浏览器作兼容性处理。
具体优化请参考:Show me the code,babel 7 最佳实践!
业务代码
拆分模块按需加载 (Code Splitting)
现在 SPA 已经是一个常见的场景了,而一般情况下,单页面应有一般都会存在多路由。而我们每次访问其实只访问一个路由,将代码按路由拆分并按需加载,对于首屏资源加载优化,是一个不错的选择。
不进行拆包需要下载 100Kb
进行拆包后只需要下载 20Kb抽离公共模块
前面提到是按路由拆分模块包,其实存在一个问题是,如果存在公共模块,那么在每一个拆分出来的路由模块都会加载这个公共模块。
路由分割后我们可以将公共模块抽离出来,避免重复的代码。
抽离公共代码后可以明显看出减少了重复的 common01.js 和 common02.js 的代码
缺点:路由动态按需加载 + 抽离公共代码可能会加载路由不必要的公共代码。例如: 访问 home 会加载 home.js + common.js(包含common01.js + common02.js),但其中的 common02.js 是没有用到的。
Webpack 4 的 splitChunksPlugin 可以根据模块之间的依赖关系,自动打包出很多很多(而不是单个)通用模块,可以保证加载进来的代码一定是会被依赖到的。
当然,打包出了多个通用模块的同时也会增加资源请求数,对前面所说的网络性能造成影响。
渲染
要优化渲染性能,根本目的就是尽快让用户看到页面内容,那么我们来看看到底用户从一片空白,到看到内容到底发生了哪些事情。
好了,了解完了整个过程,我们就可以分析其中的瓶颈以及得出优化方案了。
避免 JS 文件阻塞渲染
首先,我们从上述过程可以看到,js 资源会阻塞 HTML 的解析,那么其实以前我们的常规操作就是把 JS 资源从 head 标签移动到 body 标签的末尾,避免阻塞。
但是随着技术发展,我们已经有更好解决的方案了。
defer / async
在body内末尾添加 script 标签
HTML解析,JS资源下载,JS执行 顺序图从上图中我们可以看到, 不同情况下的脚本处理机制。
得出的优化选择是:
其实以上两者从效果差不多,因为放在 body 内末尾基本上 HTML Parser 也已经结束。
preload / prefetch
preload
前面说到大多数基于标记语言的资源能被浏览器的预加载器(Preloader)尽早发现,推测出页面需要下载哪些资源,但不是所有的资源都是基于标记语言的,比如一些隐藏在 CSS 和 Javascript 中的资源。当浏览器发现自己需要这些资源时已经为时已晚,所以大多数情况,这些资源的加载都会对页面渲染造成延迟(如用作字体图标 font 字体资源,CSS 内的背景图片资源等)。
现在可以通过 preload 来提前声明当前页面会需要哪些资源(preload 资源请求优先级为 highest):
<link rel=\"preload\" href=\"late_discovered_thing.js\" as=\"script\">
prefetch
当 SPA 使用了路由分割动态加载的时候,我们从一个页面跳转到另外一个页面的时候,浏览器会动态加载新页面所需要的 js 资源,然后再执行渲染。
使用预加载技术(prefetch) 技术能提前下载即将需要的资源。
它的原理是:
利用浏览器的空闲时间去先下载用户指定需要的内容,然后缓存起来,这样用户下次加载时,就直接从缓存中取出来,效率就快了。
<!-- 提前下载好 user 模块的 js 资源,用户访问 /user 时就可以直接读缓存 -->
<link href=\"/static/js/user.479d709b.js\" rel=\"prefetch\">
总结:
PS: prefetch 可能存在的风险:http 1.1 存在请求并发限制,如果 prefetch 数量太多,有可能阻塞异步加载的 script 资源
预渲染
前面提到,HTML 文件下载下来后,因为要等待文件加载,JS 解析等过程,而这些过程比较耗时,导致用户会长时间出于不可交互的首屏白屏状态。
在这个白屏阶段,可以用 预渲染 提前展示一部分内容,让用户感知到网站正在正常加载,而非糟糕的白屏体验。
预渲染原理:在 index.html 的 <div id=\"app\">...</div> 填充自定义的内容,在 JS 资源下载之前展示必要的内容。让页面看起来很快,实际的加载速度并没有变化
我们清楚了预渲染的原理,下面介绍下预渲染有哪几种类型。
loading 动画
首先,很简单,也很常见,在 index.html 页面内联一个 loading 动画。
loading 动画可以很简单,一个菊花图即可。
也可以很复杂,如 Google Mail 的加载动画。
这其实只是一个CSS动画,进度条也是假的,并不是真实的加载进度,动画目的是让用户知道网页正在加载中,而不是看到一片不知道是不是挂了的白屏。
渲染静态DOM
加载动画,依然会让用户知道自己在等待,那我们何不直接给 index.html 添加我们真实页面的 HTML 呢?
Prerender SPA Plugin 就是能帮我们实现这个功能的 webpack 插件。
其原理是: 在构建的时候启动模拟的浏览器环境(headless chrome),并通过预渲染的事件钩子获取当前的页面内容,生成最终的 HTML 文件。
上述gif 图中,左边用了静态预渲染,右边是常规的单页面,可以明显看到,虽然最终表现一样,但明显使用了预渲染优化的页面,会看起来快很多。
优点:这样就能让用户感受不到 loading, “误以为”自己的页面已经成功打开了,给用户很快打开页面的错觉。但页面实质上还在加载 JS 内容。
缺点:
骨架屏
上面那种方案提到了一个缺点是,基于动态数据的UI不能展示完全,例如一个账单列表,使用 Prerender SPA Plugin 预渲染的话,账单列表将会是空列表。 那么,这种情况有什么合适的方案解决呢?
骨架屏!
骨架屏是根据构建出来的页面结构,构造出页面的基本骨架内容。
自动生成骨架屏和 预渲染静态DOM 原理差不多,都是 在构建的时候启动模拟的浏览器环境(headless chrome), 获取到 HTML,但是骨架屏在此 HTML 基础上,根据一定的规则,将 HTML 中的 UI 用 灰色块替代。
优点:可以预渲染基于动态数据渲染出来的内容。
缺点:目前开源社区暂时没有一个高稳定性,高可用性的骨架屏自动生成插件。可能需要在业务代码上插入 骨架屏组件,侵入性比较强。
从网络请求,到资源下载,最后到页面渲染,整体个人探索出来的优化到此为止,其实基本上都是基于构建角度来实现的优化,当然还有更颗粒到代码层级的优化,入用 Web Worker处理长耗时的JS任务避免阻塞之类的,这里不再细说。
另外,有不同意见的,或者还有哪些重要的优化操作我没有提及的,欢迎留言,互相学习。
点赞+转发,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓-_-)
关注 {我},享受文章首发体验!
每周重点攻克一个前端技术难点。更多精彩前端内容私信 我 回复“教程”
原文链接:https://github.com/SunshowerC/blog/issues/9
作者:晨微雨
"留言与评论(共有 0 条评论) |