Extreme Server Side Rendering
Extreme Server Side Rendering (极致服务端渲染)
What is (Normal) Server Side Rendering?
什么是(常规)服务端渲染?
Before we get into extreme server side rendering (XSSR), we have to talk about normal server side rendering (SSR). This comes in two flavours, which I’m calling old-school and new-school. 在深入探讨极致服务端渲染(XSSR)之前,我们必须先谈谈常规的服务端渲染(SSR)。它主要有两种形式,我将其称为“旧派”和“新派”。
Old-school SSR involves having a server which uses some logic to create the HTML of the web page on-the-fly. For example, you might hit /users/39, and it might give you the details of user 39. These details might be from a database, or they might come from somewhere else. The important part is there’s no corresponding 39.html on the disk. The HTML is created dynamically by the back-end server. On the front-end side, there’s no JavaScript or other logic required to render the page. As a result, once the page is loaded, there’s no ability for it to be dynamic.
“旧派”SSR 指的是服务器通过某种逻辑实时生成网页的 HTML。例如,当你访问 /users/39 时,它会返回用户 39 的详细信息。这些信息可能来自数据库,也可能来自其他地方。关键在于磁盘上并没有对应的 39.html 文件,HTML 是由后端服务器动态创建的。在前端,不需要 JavaScript 或其他逻辑来渲染页面。因此,页面一旦加载完成,就无法再进行动态更新。
New-school SSR is similar to old-school SSR, but it does involve a bit of front-end JavaScript logic. Generally, the way it’s implemented is that when the user interacts with the web page, the JS will make a request to the back-end. Then, the back-end will return some HTML, and the JS front-end will insert that HTML somewhere into the page. This allows us to have dynamic pages, at the cost of having to maintain some front-end logic. “新派”SSR 与旧派类似,但它涉及少量的前端 JavaScript 逻辑。通常的实现方式是:当用户与网页交互时,JS 会向后端发起请求,后端返回一段 HTML,然后前端 JS 将这段 HTML 插入到页面中。这使我们能够实现动态页面,代价是必须维护一些前端逻辑。
Wouldn’t it be nice if there was a better way? 如果有一种更好的方法,那该多好?
Very slow HTTP servers
非常缓慢的 HTTP 服务器
As a quick aside, let’s talk about HTTP servers! When you make an HTTP request, what actually happens? Well, the server returns a bunch of ASCII text corresponding to some headers and the actual HTML content itself. This is sent one character at a time. Well, there’s some buffering, but we can ignore this as it’s not relevant to our purpose. 顺便提一下 HTTP 服务器!当你发起 HTTP 请求时,实际发生了什么?服务器会返回一堆 ASCII 文本,包含一些头部信息和实际的 HTML 内容。这些内容是一个字符一个字符发送的。虽然会有一些缓冲,但我们可以忽略它,因为它与我们的目的无关。
What happens if we send the HTML slowly, like at human rates? Below is a video where I use netcat to be a human HTTP server, and I have a browser connect to me. Netcat is line-buffered, meaning that each line is sent to the browser when I press enter. You’ll notice that the page renders on-the-fly as new HTML comes in. This is awesome, and it even works across both browsers I tested (Firefox and Chromium). Furthermore, even CSS works, so you could conceivably constantly move elements around (or hide existing elements and then add new ones). So, if we keep the HTTP connection open indefinitely, we can dynamically update the page from the server, without any JavaScript!
如果我们以人类阅读的速度缓慢发送 HTML 会怎样?下面是一个视频,我使用 netcat 充当“人类 HTTP 服务器”,并让浏览器连接到它。netcat 是行缓冲的,这意味着当我按下回车键时,每一行都会发送给浏览器。你会注意到页面随着新 HTML 的到来而实时渲染。这太棒了,而且在我测试的两个浏览器(Firefox 和 Chromium)中都能正常工作。此外,CSS 也能生效,因此你可以想象通过不断移动元素(或隐藏现有元素并添加新元素)来实现动态效果。所以,如果我们无限期地保持 HTTP 连接,就可以在没有 JavaScript 的情况下从服务器动态更新页面!
Getting User Input
获取用户输入
Updating the page from the server is cool, but it would be nice to accept user input as well. The normal way to do this without JS is a <form> element. Unfortunately, this requires a page load, which defeats the point of XSSR! If we’re willing to deal with reloading the page, normal old-school SSR would work fine. My friend DJ Chase pointed out a pretty cool trick, though:
从服务器更新页面很酷,但如果能接受用户输入就更好了。在没有 JS 的情况下,通常的做法是使用 <form> 元素。不幸的是,这需要重新加载页面,这违背了 XSSR 的初衷!如果我们愿意接受页面重载,那么常规的旧派 SSR 就能很好地工作。不过,我的朋友 DJ Chase 指出了一个非常巧妙的技巧:
<iframe style="display: none;" name="transFrame" id="transFrame"></iframe>
<form target="transFrame" method="POST" action="/action">
<input type="hidden" name="uuid" value="USER-UUID-GOES-HERE" />
<input type="Submit" value="" />
</form>
Basically, you can point a form at an iframe, which will cause that iframe to be reloaded on form submission instead of the main page. If you hide that iframe, then user input is completely seamless! 基本上,你可以将表单指向一个 iframe,这样在提交表单时,只有该 iframe 会重新加载,而不是整个主页面。如果你隐藏了这个 iframe,那么用户输入的过程就完全是无缝的!
Also worth pointing out is the uuid value, which can be generated by the server on initial page load. This is used to tie events to the original page (to prevent mixing up users when responding to events). 另外值得一提的是 uuid 值,它可以在页面首次加载时由服务器生成。它用于将事件绑定到原始页面(以防止在响应事件时混淆用户)。
Building something with XSSR
使用 XSSR 构建应用
Okay, so in theory we have everything we need to build a responsive web app. What should we build? Why not a Flappy Bird clone? (Source code is available here.) 好了,理论上我们已经具备了构建响应式 Web 应用所需的一切。我们该构建什么呢?为什么不做一个 Flappy Bird 的克隆版呢?(源代码可在此处获取。)
There’s a few things to point out here. First of all, notice that we have everything a “real” Flappy Bird clone needs. We have dynamic text for the score, the pipes move as expected, and clicking causes the bird to jump. The latter is accomplished using the iframe trick above, combined with a hidden button that covers the entire screen. This allows you to click anywhere - in fact, after clicking once to select the invisible button, spacebar and enter both work to continue clicking. 这里有几点需要说明。首先,请注意我们拥有“真正”的 Flappy Bird 克隆版所需的一切。我们有动态的分数文本,管道按预期移动,点击会让小鸟跳跃。后者是通过上述 iframe 技巧,结合覆盖整个屏幕的隐藏按钮实现的。这允许你在任何地方点击——事实上,在点击一次选中那个不可见的按钮后,空格键和回车键也可以用来继续点击。
Second of all, notice that the page never stops loading. This is because the physics engine runs entirely in the back-end, and new CSS is constantly being sent to update the position of the pipes, the bird, and the content of the score text at 60 FPS. Again, no front-end JS is required - turn it off and notice the site works exactly the same! You can also try curl-ing the page to see the infinite CSS in all its glory.
其次,请注意页面永远不会停止加载。这是因为物理引擎完全在后端运行,新的 CSS 不断被发送,以 60 FPS 的速度更新管道、小鸟的位置以及分数文本的内容。同样,不需要任何前端 JS——关掉它,你会发现网站运行得完全一样!你也可以尝试用 curl 访问该页面,看看那无穷无尽的 CSS。
This isn’t as bad as you might think
这并没有你想象的那么糟糕
Performance 性能
Performance, both client-side and server-side, is actually not awful. I will concede that the site does get laggy after a few minutes on both Firefox and Chromium. That said, we are streaming at 60 FPS, and for a page that updates e.g. only after user interaction, this might not even be a practical issue. 客户端和服务器端的性能其实并不差。我承认在 Firefox 和 Chromium 上运行几分钟后,网站确实会变得卡顿。话虽如此,我们是以 60 FPS 的速度进行流式传输的,而对于那些仅在用户交互后才更新的页面,这甚至可能根本不是问题。
Server-side is even better. On my simple Rust server implementation, I can run about 500 games at the same time per core on a relatively old server in my homelab. This is significantly better than even a moderately-complex Ruby on Rails app! (I say this with love - I love Ruby on Rails). If you lowered the framerate, or used XSSR for a page that only updates after user interaction, performance could be increased significantly. 服务器端的表现甚至更好。在我简单的 Rust 服务器实现中,在我家庭实验室里一台相对老旧的服务器上,每个核心可以同时运行约 500 个游戏。这比中等复杂度的 Ruby on Rails 应用要好得多!(我是带着爱说这话的——我热爱 Ruby on Rails)。如果你降低帧率,或者将 XSSR 用于仅在用户交互后才更新的页面,性能还可以显著提升。
Bandwidth 带宽
Bandwidth usage is… entirely reasonable! One game takes about 20KiB/s of bandwidth to run. For perspective, this means it would take about 49 seconds of gaming to use the same bandwidth as loading a 1MB JavaScript application (which unfortunately is on the smaller side these days). Again, cutting down the framerate would improve this significantly. 带宽使用量……非常合理!运行一个游戏大约需要 20KiB/s 的带宽。换个角度看,这意味着玩 49 秒游戏所消耗的带宽,才相当于加载一个 1MB 的 JavaScript 应用(不幸的是,这在今天还算小的)。同样,降低帧率可以显著改善这一点。
Latency 延迟
oh no. 噢,不。
So, depending on where you are in the world, that demo might have majorly sucked. This is because every time you click, it requires a round trip to the server - both to update the physics engine on the server with your action, and then to return the new position of the bird to your browser. This is almost entirely dominated by the on-the-wire transit time. As a result, the demo is extremely p… 所以,根据你所在地理位置的不同,那个演示可能会非常糟糕。这是因为每次点击都需要往返服务器一次——既要用你的操作更新服务器上的物理引擎,又要将小鸟的新位置返回给你的浏览器。这几乎完全受限于网络传输时间。因此,这个演示极其……