有很多工具能帮助你监控SPA的性能。首先,可以利用Chrome自带的开发者工具(Devtool)或者特定的插件。
这些工具的弊端是,他们不能准确的测出 SPA 应用的加载速度。
为了能够真正的测出 SPA 的真实加载速度,在Chrome 中也存在一些子工具(如:Speed Index)用于模拟用户真正的上网过程。这里有一篇关于Speed Index的文章可以参考一下。
但是,真实的用户操作受各种设备和网络影响,很难利用单一的插件和工具进行模拟。
所以,我们可以使用 真实用户模拟 Real User Monitoring (RUM)对应用进行处理。他能很好的跟踪用户在网页中的各种操作并且能够给出网站的实时加载数据情况。
这里列出一些针对SPA的RUM工具(有些地址,需要🪜)
在进行RUM处理的过程中,需要我们能够区分并识别「页面导航阶段」和「页面加载完成阶段」
❝页面导航阶段:在浏览器页面加载过程中发生的阶段 1. 应用被加载后 2. 用户点击用于加载新页面的链接或者按钮。 ❞
有很多方式来区分这两个阶段:
实践证明,上述的解决方案是无法提供精确结果。例如,即使没有发生新页面的加载,也可以在SPA的页面中通过AJAX来进行数据获取。又或者网络请求由于传输路径中某些原因,产生了数据丢失,但是在页面中是不会受网络波动的影响。
我们可以利用简单的API来检测页面加载的时间信息。
var rumObj = new RumTracking({
'web-ui-framework': 'EMBER'
});
// App 被加载 - window.performance.timing.navigationStart是一个加载开始的标志
rumObj.setPageKey('feed_page_key');
// 做页面渲染处理
rumObj.appRenderComplete();
// 页面导航的时间监听
rumObj.appTransitionStart();
rumObj.setPageKey('profile_page_key');
rumObj.appRenderComplete();
上面的方式可以统计页面加载信息,但是需要每个页面都需要写指定的注入逻辑。
许多SPA的JS框架都有特定的「生命周期」,我们可以利用这个机制添加上述的检测代码。
// 在页面导航开始的时候,添加监听逻辑
router.on('willTransition', () => {
a.rumObj.appTransitionStart();
});
// 页面结束的生命周期中,新增监听逻辑
router.on('didTransition', () => {
Ember.run.scheduleOnce('afterRender', () => {
a.rumObj.appRenderComplete();
});
});
开发者能够通过Navigation中指定的navigationStart
来监测页面导航何时开始。路由的willTransition
的事件会在页内导航发生时被触发。
通过侦听didTransition
事件并在afterRender
队列中添加回调,我们就可以知道在两种模式下页面何时完全加载。
现在我们可以获取到页面加载的各项时间。为了能够更好的发现页面加载的瓶颈,我们需要利用RUM数据来进行分析和处理。
也就是「优先渲染首屏」的页面信息。
如果你的SPA在渲染阶段耗费了很多时间,那么针对非首屏页面的惰性渲染是不可忽视的步骤。在渲染阶段,HTML解析器将页面中所有HTML转换为DOM对象,并生成对应的DOM树。
由于,HTML的解析在浏览器主线程的靠前位置,所以如果构建过多DOM(当前页面的所有元素都被解析)就会「阻塞」浏览器主线程。然后导致应用加载时间过长。
另一个能够加速渲染速度的方式就是为每个组件赋予不同的「渲染优先级」。
❝高优先级(绿色框):总是被渲染。该层的元素为在可视范围的所有组件。 次优先级(黄色): 该层的组件是增量渲染的。 低优先级(红色): 位于可视范围之外的组件,只有在用户滚动页面,到他们所在的位置,才会被渲染 ❞
这种处理方式可以提高Frist Meaningful Paint (FMP)的指标。(也就是「缩短」了用户能够看到页面「核心内容」的时间)。
通过对不可见元素的过滤渲染(不渲染) 也能提高Time to Interactive(TTL)的性能指标。
在优化了渲染阶段的性能后,我们继续按照渲染流水线往下走。发现「转换阶段」也可能存在性能瓶颈。
在此阶段,SPA加载数据并且对数据进行「序列化」(normalizes)处理,然后将处理完的数据存入到内存中。为了优化该阶段,减少数据量是一个很好的优化方案。
可以使用一个高优先级调用来获取First Meaningful Paint所需的数据,并使用另一个回调来「惰性加载」页面所需的其余数据。
「上述方式既适用于启动模式,也适用于页内导航,因为它们减少了前端时间」。
一些SPA框架,例如(React/Vue)是允许开发者将应用代码分割成很多bundles。所以,你就可以对一些非必要的bundles进行「按需加载」或者延迟处理。该方法可以加速「第一次导航」。例如,可以只加载用户可以立即访问的部分,并延迟其他所有内容(例如需要授权的部分)。
对你的SPA进行审查,从中甄别出可以在用户设备中被「缓存」的图片或者其他的静态资源。
从内存或者Web Storage获取数据所花费的时间远远小于通过HTTP请求的时间。
❝延迟是瓶颈,最快的速度莫过于什么也不传输。 ❞
「设备内存比最快的网络请求都快,所以缓存是优化的必要手段」。
对于大量的集合,可以使用某种类型的分页并依赖于服务器来实现持久性,或者编写LRU算法来从存储中删除多余的项。
或者使用Service Workers在SPA中缓存静态内容。
它是在后台运行的「客户端脚本」。你可以使用它们来减少流量并启用离线功能。当浏览器请求内容时,它首先通过service worker。如果请求的内容存在于缓存中,service worker将检索它并显示在屏幕上。在其他情况下,它将从网络请求资源。
你可以使用IndexedDB API缓存大量「结构化」的数据。
❝WebSocket 可以实现客户端与服务器间双向、基于消息的文本或二进制数据传输。它是浏览器中最靠近套接字的 API。 ❞
与HTTP不同,客户端不必不断地向服务器发送请求以获取新消息。相反,浏览器只需监听服务器,并在准备好时接收消息。
大部分应用需要从第三方获取数据。
但是,由于同源策略,不能对非同源的第三方服务进行AJAX调用。
❝一个“源”由应用协议、域名和端口这三个要件共同定义。 比如,(http, wl.com, 80)和(https, wl.com, 443)就是不同的源 ❞
同源策略的出发点很简单:浏览器存储着用户数据,比如认证令牌、cookie 及其 他私有元数据,这些数据不能泄露给其他应用。如果没有「同源沙箱」,那么 wl. com 中的脚本就可以访问并操纵 third-party.com 的用户数据。
为了能够访问第三方网站,应用需要利用origin server作为代理。
额外的「往返」意味着更多的延迟。
如果不处理检索到的数据,也不将其存储在系统中,则可以直接请求资源。为此,可以使用JSONP
或跨来源资源共享(CORS)进行数据获取。
网页添加一个<script>
元素,向服务器请求一个脚本
<script src="http://api.foo.com?callback=bar"></script>
❝请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称(bar) ❞
服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...}))
客户端会将服务器返回的字符串,作为代码解析,因为浏览器认为,这是<script>
标签请求的脚本内容。这时,客户端只要定义了bar()
函数,就能在该函数体内,拿到服务器返回的 JSON 数据。
网页动态插入<script>
元素,由它向跨域网址发出请求。
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面代码通过动态添加<script>
元素,向服务器example.com发出请求。
❝注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于 JSONP 是必需的。 ❞
服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
foo({
'ip': '8.8.8.8'
});
❝JSONP 只能是GET请求 ❞
同时,我们可以使用async
和 defer
属性来对<script>
进行优化处理。
属性 | 解释 |
---|---|
没有 defer 或 async | 浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行 |
async | 加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步) |
defer | 加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成 |
CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。
但是,除了GET
、HEAD
和POST
之外,使用任何方法的请求都会发起一个「预检请求」(preflight check),以确认服务器已经为跨源请求做好了准备。
为了做预检请求,客户端发送「另一个请求」,描述源、方法和跨源AJAX调用的头。根据这些信息,服务器决定是否处理该调用。客户端收到响应后,向第三方资源发起请求。
=> 预备请求
OPTIONS /resource.js HTTP/1.1 ①
Host: thirdparty.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: My-Custom-Header
...
<= 预备响应
HTTP/1.1 200 OK ②
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: My-Custom-Header
...
(正式的 HTTP 请求) ③
「预检请求」多了一次往返时间,无形中加大了请求的延迟时间。
CDN 是内容交付网络 (Content Delivery Networks) 的英文首字母缩写,它能将内容传送到更靠近用户的地方,让在线体验更高速、更可靠。
内容交付网络 (CDN) 是一组分布在「不同地理位置」的服务器,它将 Web 内容存放在更靠近用户的位置,从而加速 Web 内容的交付。全球各地的数据中心都使用缓存,这是一种「临时存储文件副本」的过程,让你可以通过距离你所在地点较近的服务器,更快速地使用支持上网的设备或浏览器访问互联网内容。
CDN 将网页、图像和视频等内容缓存在靠近你的实际地点的「代理服务器」中。
❝把 CDN 想成是一部 ATM 机。如今几乎每个街角都有提款机,让我们可以快速高效地提取现金。不用再去银行排长长的队伍,而是可以在许多便捷的地点找到 ATM,快速取到现金 ❞
「为SPA使用cdn意味着更快地加载脚本和减少交互时间」