草庐IT

前端首屏渲染时间的极致优化

温瞳 2023-03-28 原文

我们知道,用户体验是 Web 产品最为重要的部分。尽可能减少首屏加载时间,更为流畅地展示用户所需求的内容,会是用户是否留存的关键因素。

而随着现代 Web 业务可供用户的交互行为越来越多,前端项目的复杂度越来越高,每个页面的渲染时间也必然越来越长,这就导致了用户的体验不佳,用户的操作变慢。

为此,前端工程师们在首屏请求的各个阶段中持续钻研,不断探究如何将首次页面渲染的时间减少到更小,力求提供更为优秀的产品体验。

CSR(Client Side Render)

浏览器渲染是最简单,最符合 Web 应用设计思路的渲染方式。

所谓浏览器渲染,就是将应用所需的页面展示、前端逻辑、接口请求全都在用户的浏览器中执行。它很好的实现了前后端的解耦,让前端开发更为独立,也让后台实现更为简单。

同时,为了缓解用户的等待焦虑,我们可以用 loading 态,或者骨架屏,进一步提升异步请求接口时的用户体验。

不过,随着业务复杂程度提高,浏览器渲染的开销也会变大,我们无法控制用户侧使用的机器性能,很多时候,用户使用的机器性能甚至不足以满足应用的需求,造成卡顿,甚至崩溃,这一点在移动端上尤甚。

而浏览器渲染由于前端的动态性过高,也会带来 SEO 不佳的问题。

SSR(Server Side Render)

服务端渲染的出现时间实际上是要比浏览器渲染要更早的。在 Web 应用发展的早期,所有的 ASP、JSP 等模板引擎构建的前端页面实际上就是服务端渲染的结果。而此时的服务端渲染无法进行前后端职责的解耦,因此逐步被浏览器渲染淘汰。

但在处理首屏体验的问题上,服务端渲染有着独到的优势。它能提前再服务端中完成页面模板的数据填充,从而一次性返回完整的首屏内容,从而面对 SEO 的爬取时能获取到更多有效的关键信息。

此外,由于其能快速直出首页的真实数据,体验往往比 loading 态更佳,在 TTI 的表现上更为出色。

但是,服务端渲染也有其自身的局限性。因为从本质上来说,SSR 服务无法完全与前端页面解耦开来。因此市面上较完备的 SSR 解决方案都只解决首屏的服务端渲染,并采用同构的方式,增加一层 node 中间层的方式来解决前端与 SSR 服务的更新同步问题,并与后端开发项目解耦。

但这无疑增加了项目的复杂度,并且随着业务的复杂程度变高,服务端渲染往往需要调起多个接口去请求数据并填充页面,这样可能会导致在 TTFB 上有一定劣势。

当然,最重要的是,服务端渲染对于服务器的负载要求是很高的。

上图是引用的字节的某项目的 SSR 服务的单机 QPS 承载表现。我们可以看出,对于一个高访问量的网页应用来说,提供一个较为复杂的 SSR 服务的成本是相当高的,需要花费大量的金钱来堆机器。

因此,从降本增效的角度考虑,我们需要评估 SSR 带来的 ROI 是否符合预期。

NSR(Native Side Render)

在移动互联网的浪潮下,移动端机能飞速提升,那么 Web 应用是否能搭上这一班车,将 Native 的性能利用起来,提升页面渲染性能呢?答案是肯定的,这就需要介绍到 NSR 了。

Native 渲染的本质其实还是 SSR,只不过提供服务的 Server 转变为了客户端。由于需要用到客户端机能,因此此种实现通常应用在移动端 APP,或者 PWA 下。

当链接被点击时,先借助浏览器启用一个 JS 运行时,并加载 APP 中存储的 Html 模板,发送 xhr 请求预加载页面数据,从而在客户端本地拼接并渲染生成一个有数据的 Html 首屏,形成首次 NSR。同时可以将该首屏 Html 缓存在客户端,供下次页面打开时,实现 stale-while-revalidate 的缓存效果。

由于 NSR 将服务器的渲染工作放在了客户端的一个个独立设备中,既实现了页面的预加载,同时又不会增加额外的服务器压力。达到秒看的效果。

这种能力在拥有客户端或者支持 PWA 的应用中应用广泛,例如手 Q,腾讯文档 APP 中都拥有通过 APP 中的离线包来实现首屏渲染加速的能力。

ESR(Edge Side Render)

那么,对于纯 Web 应用,而又由于兼容性等原因暂时无法支持 PWA 的页面,有没有一个合适的首屏渲染加速方案呢?

随着云与边缘计算的快速发展,前端页面也需要考虑分布式的请求处理优化。

我们知道,CDN 节点相比真实服务节点更贴近用户,能更快将内容返回。因此我们可以将静态的 Html 内容模板缓存在 CDN 上。当接到请求时,先快速将静态模板页面返回给用户,同时在 CDN 服务器上对页面动态部分发起向后端发起请求,并将获取到的动态内容在以流式下发的方式继续返回给用户。

这里实际上利用到了 HTTP 的 SSE(Server Send Events)协议,通过服务器向客户端发送单向事件流,实现同一个 Html 文件的分块传输预渲染。

最佳实践是?

这也是我们最近在腾讯文档中探索实践并落地的,通过服务中间节点的流式下发能力,实现多级首屏渲染加速。

对于一个复杂前端页面来说,首屏需要加载和运算的资源类型可能有很多,有需要客户端解析并执行的 JS 动效,也有需要服务端获取数据直出的数据分片展示页面。

通常来说,客户端只能等待服务端获取分片数据,并生成经过 SSR 渲染后的 HTML,才能开始进行 script 解析与 js 资源拉取的行为,最终渲染出完整的页面数据以及动效。

而既然他们所需要的计算方式不同,那么为什么不能并行来做呢?

我们可以在版本发布前,将未经过服务端直出的模板 HTML 进行解析,将需要发起资源请求的所有的外链脚本 url 提取出来,生成一个 HTML Header 结构,并将该 Header 内容伪装为正常 HTML 缓存在 CDN 节点中。

结合之前我们介绍的 HTTP SSE 协议,当用户请求时,我们可以以最快的速度向用户返回 CDN 中的 HTML header,从而让用户的浏览器提前拉取并解析外链资源。于此同时,CDN 节点将用户的请求转发给真实的服务端,从而让服务端进行真实数据的获取拼接并返回给客户端。

由于客户端此时已经提前拉取了外链资源,因此收到服务端分片的 SSR 后,客户端可以直接将真实数据渲染到页面中,而不需要再次等待外链资源的解析。

由于并行的关系,这样的 SSR 与 NSR 混合方式能大大降低复杂页面首屏渲染的时间,提升用户体验。

以百度首页的请求为例,通过 Chorme Network 提供的瀑布图,通过我们可以直观的看到一条请求的执行过程。

我们可以看出,除了 DNS 寻址与 SSL 建连是我们无法控制的以外,占用请求时间的大头是 Waiting for server response,请求服务器 (CDN) 的时间,以及 Content Download,外链资源的拉取时间。

而使用本文的混合方案后,理论上可以使总请求时间降低到 Max(A, B), (A 为 Waiting for server response,B 为 Content Download) 的水平。(当然,实际操作过程中,由于 CDN 节点进行了一次请求转发,因此拥有 SSR 能力的页面请求返回时间会更长一些)。

结语

前端的页面首屏时间优化是永恒的话题,本文介绍了前端界对首屏时间优化的进程,并提供了一种 SSR 与 NSR 混合的新思路,通过并行处理耗时任务的方式,进一步提升首屏加载时间,希望能够给大家提供一点参考价值。

有关前端首屏渲染时间的极致优化的更多相关文章

  1. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  2. ruby-on-rails - Rails HTML 请求渲染 JSON - 2

    在我的Controller中,我通过以下方式在我的index方法中支持HTML和JSON:respond_todo|format|format.htmlformat.json{renderjson:@user}end在浏览器中拉起它时,它会自然地以HTML呈现。但是,当我对/user资源进行内容类型为application/json的curl调用时(因为它是索引方法),我仍然将HTML作为响应。如何获取JSON作为响应?我还需要说明什么? 最佳答案 您应该将.json附加到请求的url,提供的格式在routes.rb的路径中定义。这

  3. ruby-on-rails - Ruby 检查日期时间是否为 iso8601 并保存 - 2

    我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby​​是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查

  4. ruby-on-rails - 将 Ruby 中的日期/时间格式化为 YYYY-MM-DD HH :MM:SS - 2

    这个问题在这里已经有了答案:Railsformattingdate(4个答案)关闭4年前。我想格式化Time.Now函数以显示YYYY-MM-DDHH:MM:SS而不是:“2018-03-0909:47:19+0000”该函数需要放在时间中.现在功能。require‘roo’require‘roo-xls’require‘byebug’file_name=ARGV.first||“Template.xlsx”excel_file=Roo::Spreadsheet.open(“./#{file_name}“,extension::xlsx)xml=Nokogiri::XML::Build

  5. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  6. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  7. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

  8. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

  9. ruby - 以毫秒为单位获取当前系统时间 - 2

    在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题:

  10. ruby-on-rails - Rails 渲染带有驼峰命名法的 json 对象 - 2

    我在一个简单的RailsAPI中有以下Controller代码:classApi::V1::AccountsControllerehead:not_foundendendend问题在于,生成的json具有以下格式:{id:2,name:'Simpleaccount',cash_flows:[{id:1,amount:34.3,description:'simpledescription'},{id:2,amount:1.12,description:'otherdescription'}]}我需要我生成的json是camelCase('cashFlows'而不是'cash_flows'

随机推荐