前言
基于现代Web前端框架的应用,其原理是通过浏览器向服务器发送网络请求,获取必要的index.html和打包好的JS、CSS等资源,在浏览器内执行JS,动态获取数据并渲染页面,从而将结果呈现给用户。在这个过程中,有两个步骤可能较为耗时,一个是网络资源的加载,另一个是浏览器内代码执行和DOM渲染。而耗时的增加会导致页面响应慢,卡顿,影响用户体验,针对上述两种耗时的情况,常见的优化方向有:
- 缩短请求耗时;
- 减少重排重绘;
- 改善JS性能。
首先讲一下如何减少或者缩短请求耗时:
请求耗时
在不影响运行的基础上减少http请求无疑是一个很重要的前端优化。
减少http请求数
Web 前端 80% 的响应时间花在图片、样式、脚本等资源下载上。最直接的方式是减少页面所需资源,但并不现实。所以,减少 HTTP 请求数主要的途径是:
合并 JS/CSS 文件
服务器端(CDN)自动合并,基于 Node.js 的文件合并工具,通过把所有脚本放在一个文件中的方式来减少请求数。
最好的优化是做到页面只加载一个 CSS 和一个 JS。
使用 CSS Sprite
将背景图片合并成一个文件,通过 background-image 和 background-position 控制显示
- 在 Sprite 图片中横向排列一般都比纵向排列的最终文件小;
- 组合 Sprite 图片中的相似颜色可以保持低色数,最理想的是 256 色以下 PNG8 格式
- “对移动端友好”,不要在 Sprite 图片中留下太大的空隙。虽然不会在很大程度上影响图片文件的大小,但这样做可以节省用户代理把图片解压成像素映射时消耗的内存。100×100 的图片是 1 万个像素,而 1000×1000 的图片就是 100 万个像素了。
行内图片(Base64 编码)
使用 Data URI scheme 将图片嵌入 HTML 或者 CSS 中;或者将 CSS、JS、图片直接嵌入 HTML 中,会增加文件大小,也可能产生浏览器兼容及其他性能问题。
减少页面的 HTTP 请求数是个起点,这是提升站点首次访问速度的重要指导原则。
Ajax 时尽量使用 GET 方法
使用 XMLHttpRequest 时,浏览器的 POST 请求是通过一个两步的过程来实现的:先发送 HTTP 头再发送数据。所以最好用 GET 请求,它只需要发送一个 TCP 报文(除非 Cookie 特别多)。
IE 的 URL 长度最大值是 2K,所以如果要发送的数据超过 2K 就无法使用 GET 了。
POST 在不提交任何数据的情况下跟 GET 行为很类似,但从语义上讲,获取数据应该用 GET,提交数据到服务器用 POST。
使用替代@import
为了实现逐步渲染,CSS 应该放在顶部。
在 IE 中用 @import 与在底部用 <link> 效果一样,所以最好不要用它。
不要在 HTML 中缩放图片
不要使用<img>的 width、height 缩放图片,如果用到小图片,就使用相应大小的图片。如果需要
<img width="100" height="100" src="mycat.jpg" alt="My Cat" />
那么图片本身(mycat.jpg)应该是 100x100px 的,而不是去缩小 500x500px 的图片。
很多 CMS 和 CDN 都提供图片裁切功能。
补充:设置图片的宽和高,以免浏览器按照「猜」的宽高给图片保留的区域和实际宽高差异,产生重绘。
使用体积小、可缓存的 favicon.ico
Favicon.ico 一般存放在网站根目录下,无论是否在页面中设置,浏览器都会尝试请求这个文件。
所以确保这个图标:
- 存在(避免 404);
- 尽量小,最好小于 1K;
- 设置较长的过期时间。
文件不要大于 25K
这个限制是因为 iPhone 不能缓存大于 25K 的组件,注意这里指的是未压缩的大小。这就是为什么缩减内容本身也很重要,因为单纯的 gzip 可能不够。
避免图片 src 为空
图片标签的 src 属性值为空字符串可能以下面两种形式出现:
HTML:
<img src="" />
JavaScript:
var img = new Image();
img.src = "";
虽然 src 属性为空字符串,但浏览器仍然会向服务器发起一个 HTTP 请求:
IE 向页面所在的目录发送请求; Safari、Chrome、Firefox 向页面本身发送请求; Opera 不执行任何操作。 空 src 产生请求的后果不容小觑:
给服务器造成意外的流量负担,尤其时日 PV 较大时; 浪费服务器计算资源; 可能产生报错。 空的 href 属性也存在类似问题。用户点击空链接时,浏览器也会向服务器发送 HTTP 请求,可以通过 JavaScript 阻止空链接的默认的行为。
网络资源是Web应用运行的基础,改善网络资源加载速度会显著改善前端性能。
优化打包资源
总体原则: 减少或延迟模块引用,以减少网络负荷。
常用工具:
webpackwebpack-bundle-analyzer可视化分析工具
常用方法:
- 减小体积:减少非必要的import;压缩JS代码;配置服务器gzip等;使用WebP图片;
- 按需加载:可根据“路由”、“是否可见”按需加载JS代码,减少初次加载JS体积。比如可以使用import()进行代码分割,按需加载;
- 分开打包:利用浏览器缓存机制,依据模块更新频率分层打包.
其他方法:
雪碧图:每个HTTP/1.1请求都是独立的TCP连接,最大6个并发,所以合并图片资源可以优化加载速度。HTTP/2已经不需要这么做了。
CDN加速
总体原则: 通过分布式的边缘网络节点,缩短资源到终端用户的访问延迟。
常用工具:
CloudflareAWS CloudFrontAliyun CDN
常用方法:
- 加速图片视频
- 大体积文件
用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面。但具体要怎么做呢?
实现内容在地理位置上分散的第一步是:不要尝试去重新设计你的 Web 应用程序来适应分布式结构。这取决于应用程序,改变结构可能包括一些让人望而生畏的任务,比如同步会话状态和跨服务器复制数据库等事务。缩短用户和内容之间距离的提议可能被推迟,或者根本不可能通过,就是因为这个难题。
网站 80-90% 响应时间消耗在资源下载上,减少资源下载时间是性能优化的黄金法则。相比分布式架构的复杂和巨大投入,静态内容分发网络(CDN)可以以较低的投入,获得加载速度有效提升。
内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。
使用多个域名
Chrome 等现代化浏览器,都会有同域名限制并发下载数的情况,不同的浏览器及版本都不一样,简单的情况如下:
| 浏览器版本 | 每个域名并发连接数 |
|---|---|
| Chrome34/32 | 6 |
| IE10 | 8 |
| IE11 | 13 |
| Firefox27/26 | 6 |
| Safari7.0.1 | 6 |
使用不同的域名可以最大化下载线程,但注意保持在 2~4 个域名内,以避免 DNS 查询损耗,例如,动态内容放在 csspod.com 上,静态资源放在 static.csspod.com 上。这样还可以禁用静态资源域下的 Cookie,减少数据传输。
浏览器缓存
总体原则:避免重复传输相同的数据,节省网络带宽,加速资源获取。
常用方法:
可以通过设置HTTP Header来控制缓存策略,一般有如下几种。
强缓存:
Expires:HTTP/1.0Cache-Control:HTTP/1.1
协商缓存:
ETag + If-None-MatchLast-Modified + If-Modified-Since
拿ETag举例,如果浏览器给的If-None-Match值与服务端给的ETag值相等,服务器就直接返回304,从而避免重复传输数据。
如果几个配置同时存在,则优先级为:Cache-Control > Expires > ETag > Last-Modified。
Gzip 压缩
压缩组件通过减少 HTTP 请求产生的响应包的大小,从而降低传输时间的方式来提高性能。从 HTTP1.1 开始,Web 客户端可以通过 HTTP 请求中的 Accept-Encoding 头来标识对压缩的支持:
Accept-Encoding: gzip,deflate
Chrome 开发者工具栏监控到如图:
如果 Web 服务器看到请求中的这个头,就会使用客户端列出的方法中的一种来压缩响应。Web 服务器通过响应中的 Content-Encoding 头来告知 Web 客户端:
Content-Encoding: gzip
目前许多网站通常会压缩 HTML 文档,脚本和样式表的压缩也是值得的(包括 XML 和 JSON 在内的任何文本响应理论上都值得被压缩)。但是,图片和 PDF 文件不应该被压缩,因为它们本来已经被压缩了。
压缩通常能将响应的数据量减少近 70%,但是压缩通常情况下会带来服务端和客户端的 CPU 开销,要检测受益是否大于开销,需要综合考虑响应大小、带宽和客户端服务端物理距离等因素。通常需要对大于 1KB 或 2KB 的文件进行压缩。
当浏览器通过代理来发送请求时,有可能出现浏览器期望接受的压缩后内容和实际接收到的不一致的情况。解决这一问题的方法是在 Web 服务器的响应中添加 Vary 头。Web 服务器可以告诉代理根据一个或多个请求头来改变缓存的响应。由于压缩的决定是基于 Accept-Encoding 请求头的,因此需要在服务器的 Vary 响应头中包含 Accept-Encoding:
Vary: Accept-Encoding
目前大约 90% 的通过浏览器进行的网络通信都需要使用 gzip,这使得服务端和客户端的对等性变得额外重要。无论是客户端还是服务端发送错误,都会造成页面被破坏。避免错误的一种方式是采用『浏览器白名单』方式,即只为经过证实支持压缩的浏览器提供压缩内容,但是当代理缓存加进来以后,处理边缘情形浏览器将变得更加复杂。另一种方式是使用 Vary: * 或 Cache-Control: private 头来禁用代理缓存。此种方式会为所有浏览器禁用代理缓存,从而增加带宽开销。如何平衡压缩和代理支持需要在加快响应时间、减小带宽开销和边缘情形浏览器缺陷之间进行权衡:
如果网站的用户很少,并且他们处于一个小圈子中,边缘情形浏览器不需要太多关注,可以压缩内容并使用 Vary: Accept-Encoding。 如果更注重带宽开销,可以和前一种情形一样,压缩内容并使用 Vary: Accept-Encoding。 如果网站拥有大量的、多变的用户群,能够应付较高的带宽开销,并且享有高质量的声誉,需要压缩内容并使用 Cache-Control: Private。( Google 和 Yahoo 都使用这种方式)。
更高版本的HTTP
总体原则:使用高版本HTTP提升性能。
常用工具:HTTP/2
HTTP/2较HTTP/1.1最大的改进在于:
- 多路复用:单一TCP连接,多HTTP请求;
- 头部压缩:减少HTTP头体积;
- 请求优先级:优先获取重要的数据;
- 服务端推送:主动推送CSS等静态资源。
其他方法:HTTP/3
HTTP/3基于UDP,有很多方面的性能改进,如多路复用无队头阻塞,响应更快。
Web Socket
总体原则:解决HTTP协议无法实时通信的问题。
Web Socket是一条有状态的TCP长连接,用于实现实时通信、实时响应。
服务器端渲染(SSR)
总体原则:第一次访问时,服务器端直接返回渲染好的页面。
一般流程:
- 浏览器向 URL 发送请求;
- 服务器端返回“空白”
index.html; - 浏览器不能呈现页面,需要继续下载依赖;
- 加载所有脚本后,组件才能被渲染。
SSR流程:
- 浏览器向 URL 发送请求;
- 服务器端执行JS完成首屏渲染并返回;
- 浏览器直接呈现页面,然后继续下载其他依赖;
- 加载所有脚本后,组件将再次在客户端呈现。它将对现有View进行合并。
常用工具:
Node.js,用于服务器端执行代码,输出HTML给浏览器,支持所有主流前端框架Next.js,用于服务器端渲染React的框架gatsby,用React生成静态网站的工具
除了可以提升页面用户体验,还能应用于SEO。