北京最好白癜风医院怎么去 http://pf.39.net/bdfyy/bjzkbdfyy/前言
本文是RenderingontheWeb:PerformanceImplicationsofApplicationArchitecture(GoogleI/O’19)[1]这篇谷歌工程师带来的现代应用架构体系下的优化相关演讲的总结,演讲介绍了以下优化手段:
预渲染同构渲染流式渲染渐进式注水(非常精彩)应用架构体系
当我们讨论「应用架构」的时候,可以理解为通过以下几个部分组合来构建网站。
Componentmodel组件模型。Renderingandloading渲染和加载。Routingandtransitions路由和过渡。Data/statemanagement数据、状态的管理。image性能指标
在分析页面渲染性能之前,先了解一下几个比较重要的指标,方便下文理解:
FP:FirstPaint,是PaintTimingAPI的一部分,是页面导航与浏览器将该网页的第一个像素渲染到屏幕上所用的中间时,渲染是任何与输入网页导航前的屏幕上的内容不同的内容。
FCP:FirstContentfulPaint,首次有内容的渲染是当浏览器渲染DOM第一块内容,第一次回馈用户页面正在载入。
TTI:Timetointeractive第一次可交互时间,此时用户可以真正的触发DOM元素的事件,和页面进行交互。
FID:FirstInputDelay第一输入延迟测量用户首次与您的站点交互时的时间(即,当他们单击链接,点击按钮或使用自定义的JavaScript驱动控件时)到浏览器实际能够的时间回应这种互动。
TTFB:TimetoFirstByte首字节时间,顾名思义,是指从客户端开始和服务端交互到服务端开始向客户端浏览器传输数据的时间(包括DNS、socket连接和请求响应时间),是能够反映服务端响应速度的重要指标。
如果你还不太熟悉这些指标也没关系,接下来的内容中,会结合实际用例分析这些指标。
渲染开销Thecostofrendering客户端渲染Client-siderendering
从服务端获取HTML、CSS、JavaScript都是需要成本的,以一个CSR(客户端渲染)的网站为例,客户端渲染的网站依赖框架库(bundle)、应用程序(app)来进行初始化渲染,假设它有1MB的JavaScriptBundle代码,那么只有当这一大段的代码加载并执行完成以后,用户才能看到页面。
它的结构一般如下:
分析一下它的流程:
用户输入网址进入网站,拉取HTML资源。HTML资源中发现script标签加载的bundle再一次发起请求拉取bundle。此时也是性能统计指标中的FP完成。
在这个阶段,页面基本上是没什么意义的,当然你也可以放置一些静态的骨架屏或者加载提示,来友好的提示用户。
JavaScriptbundle下载并执行完毕,此时页面才真正渲染出有意义的内容。对应FCP完成。
当框架对DOM节点添加各类事件绑定后,用户才真正可以和页面交互,此时也对应TTI完成。
它的缺点在于,直到整个JavaScript依赖执行完成之前,用户都看不到什么有意义的内容。
服务端同构渲染SSRwithHydration
基于以上客户端渲染的缺点以及用户对于CSR应用交互更加丰富的需求,于是诞生了集SSR和CSR的性能、SEO、数据获取的优点与一身的「同构渲染」,简单点说,就是:
第一次请求,在服务端就利用框架提供的服务端渲染能力,直接原地请求数据,生成包含完整内容的html页面,用户不需要等待框架的js加载就可以看到内容。
等到页面渲染后,再利用框架提供的Hydration(注水)能力,让服务端返回的“干瘪”的HTML注册事件等等,变的丰富起来,拥有了各种事件后,就和传统CSR一样拥有了丰富多彩的客户端交互。
在同构应用中,只要HTML页面返回,用户就可以看到丰富多彩的页面:
而JavaScript加载完毕后,用户就可以和这些内容进行交互(比如点击放大、跳转页面等等……)
代码对比
典型的CSRReact应用的代码是这样的:
而SSR的代码则需要服务端的配合,
先由服务端通过ReactDOMServer.renderToString在服务端把组件给序列化成html字符串,返回给前端:
前端通过hydrate注水,使得功能交互变的完整:
Vue的SSR也是同理:
同构的缺陷
至此看来,难道同构应用就是完美的吗?当然不是,其实普通的同构应用只是提升了FCP也就是用户看到内容的速度,但是却还是要等到框架代码下载完成,hydrate注水完毕等一系列过程执行完毕以后才能真正的可交互。
并且对于FID也就是FirstInputDelay第一输入延迟这个指标来说,由于SSR快速渲染出内容,更容易让用户误以为页面已经是可交互状态,反而会使「用户第一次点击-浏览器响应事件」这个时间变得更久。
因此,同构应用很可能变成一把「双刃剑」。
下面我们来讨论一些方案。
Pre-rendering预渲染。
对于不经常发生变化的内容来说,使用预渲染是一种很好的办法,它在代码构建时就通过框架能力生成好静态的HTML页面,而不是像同构应用那样在用户请求页面时再生成,这让它可以几乎立刻返回页面。
当然它也有很大的限制:
只适用于静态页面。需要提前列举出需要预渲染的URLs。流式渲染Streaming
流式渲染可以让服务端对大块的内容分片发送,使得客户端不需要完整的接收到HTML,而是接受到第一部分时就开始渲染,这大大提升了TTFB首字节时间。
在React中,可以通过renderToNodeStream来使用流式渲染:
渐进式注水ProgressiveHydration
我们知道hydrate的过程需要遍历整颗React节点树来添加事件,这在页面很大的情况下耗费的时间一定是很长的,我们能否先只对关键的部分,比如视图中可见的部分,进行「注水」,让这部分先一步可以进行交互?
想象一下它的特点:
组件级别的渐进式注水。服务端依旧整页渲染。页面可以根据优先级来分片“启动”组件。
通过一张动图来直观的感受一下普通注水(左)和渐进式注水(右)的区别:
可以看到用户第一次可以交互的时间大大的提前了。
光说不做假把式,我们看看用React完成这个功能的代码,首先我们需要准备一个组件Hydrator用来实现当某个组件进入视图范围以后再进行注水。
首先来看看应用的整体结构:
letload=()=import(./stream);letHydrator=ClientHydrator;if(typeofwindow===undefined){Hydrator=ServerHydrator;load=()=require(./stream);}exportdefaultfunctionApp(){return(divid="app"Header/Intro/Hydratorload={load}//div);}
根据客户端和服务端的环境区分使用不同的Hydrator,在服务端就直接返回普通的html文本:
functioninteropDefault(mod){return(modmod.default)
mod;}exportfunctionServerHydrator({load,...props}){constChild=interopDefault(load());return(sectionChild{...props}//section);}
而客户端,则需要实现渐进式注水的关键部分:
exportclassHydratorextendsReact.Component{render(){return(sectionref={c=(this.root=c)}dangerouslySetInnerHTML={{__html:}}suppressHydrationWarning/);}}
首先render部分,利用dangerouslySetInnerHTML来使得这部分初始化为空的html文本,并且由于server端肯定还是和往常一样全量渲染内容,而客户端由于初始化需要先不做任何处理,会导致React内部对于服务端内容和客户端内容的「一致性检测」失败。
而利用dangerouslySetInnerHTML的特性,会让React不再进一步hydrate遍历children而是直接沿用服务端渲染返回的HTML,保证在注水前渲染的样式也是OK的。
再利用suppressHydrationWarning取消React对于内容一致性检测失败的警告。
exportclassHydratorextendsReact.Component{
转载请注明:http://www.qianzhongdushijian.com/jdff/6446.html