几个星期以前,我在希思罗机场飞机起飞之前忙点事情, 然后我发现github的表现有点奇怪:在新页面打开链接 比直接在当前页点击链接打开要快,这是我当时录下的
background-image: url("https://i.ytimg.com/vi_webp/4zG0AZRZD6Q/sddefault.webp"); 这里我点击了一个链接,然后在一个新页面把这个链接粘贴进去,虽然这个页面是后打开的,但新页面渲染的更快。
当你加载一个页面时,浏览器获取一个网络流,然后加载进html parser中,然后再将转换的内容输出到document中。 这意味着页面可以在下载的同时一点一点的加载,这个页面可能有100k大,但是可以渲染内容即使只接受到20k的数据。 这是一个伟大的、久远的浏览器特性,但是作为开发者我们经常抛弃这种特性。大多数加载时的性能建议可以归结为“你得到多少 就展示多少”,不要停滞,不要等到所有东西都加载好后才展示给用户看。 GitHub关心性能,所以使用服务器端渲染页面。然而在同一个tab页面跳转就是使用javascript重新实现了一遍,就想下面代码这样。。// …lots of code to reimplement browser navigation…
const response = await fetch('page-data.inc');
const html = await response.text();
document.querySelector('.content').innerHTML = html;
// …loads more code to reimplement browser navigation…
这不符合规则,因为所有的`page-data.inc'会预先下载在所有东西完成之前
服务器端渲染不会以这种方式保存内容,它会以数据流的形式加速渲染。对于GitHub的客户端渲染,大量的javascript代码使这个
变得缓慢。
我在这里只是用GitHub作为一个例子,这种不好的模式几乎在所有的单页面应用内都在使用。
在页面内切换内容有一些优点,尤其是页面内加载了很大很重的scripts,因为你可以更新页面
内容而不用重新部署所有的js。但是我们可不可以那样做而且继续使用streaming?我以前总说
JavaScript不能访问stream parser,但它却是。。
最糟糕的是涉及到了<iframe>
,而且这一次用到了<iframe>
和document.write
但确实实现了以数据流的形式加载页面,代码是这样的:
// Create an iframe:
const iframe = document.createElement('iframe');
// Put it in the document (but hidden):
iframe.style.display = 'none';
document.body.appendChild(iframe);
// Wait for the iframe to be ready:
iframe.onload = () => {
// Ignore further load events:
iframe.onload = null;
// Write a dummy tag:
iframe.contentDocument.write('<streaming-element>');
// Get a reference to that element:
const streamingElement = iframe.contentDocument.querySelector('streaming-element');
// Pull it out of the iframe & into the parent document:
document.body.appendChild(streamingElement);
// Write some more content - this should be done async:
iframe.contentDocument.write('<p>Hello!</p>');
// Keep writing content like above, and then when we're done:
iframe.contentDocument.write('</streaming-element>');
iframe.contentDocument.close();
};
// Initialise the iframe
iframe.src = '';
尽管<p>Hello!</p>
写在了iframe
里边,但它出现在父文档里面!这是
因为html parser中保留了一
个打开的元素的堆栈 (stack of open elements
记录html元素的匹配,<xx>
和</xx>
,这就是一对打开和关闭的元素)新创建的元素会插入到堆栈中(目测是栈顶,后进后出)
我们怎么操作<streaming-element>
并不影响,它就是有效的。
同样的,这个方法处理html比innerHTML
更加贴近标准的页面加载解析器。
显然,scripts会父文档的上下文中下载和执行,然而在火狐中根本不执行,原因是script不应该被执行
但是在Edge, Safari和 Chrome 中都适用。
现在我们只需从服务器stream数据,然后在数据到来时使用iframe.contentDocument.write()
写入数据。
使用fetch()
会非常高效的stream数据,然而Safari并不支持,可以使用XHR来代替。
我做了一个小测试可以和GitHub所采用的方式做比较,下面是基于3g的测试结果
将内容通过数据流的方式加载进来要比xhr+innerHTML要快1.5s左右,所有内容加载完要提前0.5s,stream意味着浏览器
发现他们会比较早,所以可以一边下载一边渲染。
GitHub能够实现是因为服务器处理html,但是如果你使用一个框架,用自己的方式来表达DOM,这就行不通了,
对于这种情况,下面这种方法是一个折中的做法:
很多网站使用json的格式传递动态的更新项。不幸的是JSON并不是一个streaming-friendly(流友好)的格式。
虽然有JSON的streaming JSON parsers(JSON格式转streaming格式的工具),
但是这个并不好用。
所以不采用传递成片的JSON:
{
"Comments": [
{"author": "Alex", "body": "…"},
{"author": "Jake", "body": "…"}
]
}
而是传递JSON对象在新的一行里:
{"author": "Alex", "body": "…"}
{"author": "Jake", "body": "…"}
这就是“newline-delimited JSON”(换行符JSON),这是它的定义描述。 写一个这样的解析器要容易得多,在2017年我们将可以写出下面这种流的变换以及组合:(这里感觉可以用JSON.stringify())
const response = await fetch('comments.ndjson');
const comments = response.body
// From bytes to text:
.pipeThrough(new TextDecoder())
// Buffer until newlines:
.pipeThrough(splitStream('\n'))
// Parse chunks as JSON:
.pipeThrough(parseJSON());
for await (const comment of comments) {
// Process each comment and add it to the page:
// (via whatever template or VDOM you're using)
addCommentToPage(comment);
}
这里splitStream
和parseJSON
是可复用的stream的变换,与此同时,为了让大多数浏览器兼容
我们可以从XHR上开始。
再一次的,我给出了一个小测试用例来比较下
面是基于3g的测试结果:
和传统的JSON相比,ND-JSON将内容呈现在屏幕上要比传统的快1.5s,尽管它并没有iframe这种方式快,它
需要等待JSON完整的加载完才能去创建元素,你会遇到lack-of-streaming(缺少流)的问题如果JSON对象非常大。
正如我上面所说的,GitHub写了很代码来解决这个性能问题。在客户端上重新实现链接是很困难的,而且如果这个页面内
要替换的东西比较大的话就不值得这么做了。
如果我们比较一下一个简单的浏览器导航
。。。打开一个简单的不使用js通过服务器渲染的页面和上面的差不多一样快,除了评论列表,测试页面非常简单, 如果您在页面之间重复了大量复杂的内容,您的结果可能会有所不同(基本上,我的意思是可怕的广告脚本),多测试! 你可能写了很多代码然而只是提升了一点点性能,或者性能更差。