由于新用户可能从未与网站建立连接,从DNS查询到TCP连接,再到下载服务器返回的内容,这些步骤的耗时通常远远超过服务器的响应时间。而多数情况下开发者无法通过代码优化来减少这部分时间消耗。
为了解决新用户访问网站时可能遇到的网络开销问题,我们可以借助多种预加载技术在用户实际需要之前提前加载资源,从而减少等待时间,实现更流畅的用户体验。接下来本文将详细探讨几种常见的预加载方法,并在prefetch、preload等基础上,结合流式渲染、HTTPEarlyHints、HTTP/2push等技术,对预加载技术灵活运用,从而在用户到达网站的瞬间就提供无缝、快速的访问体验。
二、CDN动态加速在开始介绍预加载之前,其实开发者可以通过CDN动态加速优化用户与服务器的建连、内容传输时间。CDN通常被用来加速静态资源的传输,比如图像、JavaScript和CSS,这个大部分开发者非常熟悉,但现代的CDN技术已经不仅仅局限于静态内容的优化,大部分CDN厂商可以利用其全球广泛分布的边缘节点服务器为网站提供动态内容的访问加速。
用户访问网站动态内容需要通过互联网连接到源站服务器,这个过程中数据需要经过多个网络节点和长距离传输,容易受到各种网络拥塞和延迟的影响。
使用CDN动态加速时,CDN通过在全球分布的边缘节点缓存和处理用户请求,显著缩短了从用户到服务器的物理距离,减少了传输延迟。同时CDN服务商会实时监控全球的网络状态,通过智能路由技术选择当前最优的路径传输数据,这避免了网络中的拥塞和瓶颈,确保数据以最快的速度传输到用户端。
当然如果使用了CDN提供的边缘计算能力,可以让用户直接从CDN边缘节点获取动态内容,进一步加速动态内容的访问。
三、dns-prefetchDNS预解析当浏览器需要访问特定域名时,必须先将先将域名解析为IP地址,这一步骤就是DNS解析。dns-prefetch可以让浏览器提前在后台完成这一解析工作,避免用户在实际请求资源时等待DNS解析的时间。
linkrel="dns-prefetch"href="//"四、preconenct域名预建连
除了对域名进行解析、建连,还可以通过preload和prefetch对页面将要使用的资源提前下载。
preload是一种声明式资源引入方式,用来强制浏览器在合适的时机加载指定资源,通常用于关键资源(如字体、脚本、样式表等)的预加载,以确保这些资源能够尽快被使用。
linkrel="preload"href=""as="style"linkrel="preload"href=""as="script"linkrel="preload"href=""as="image"
prefetch同样是一种声明式资源请求方式,用于提示浏览器在空闲时下载未来可能用到的资源,适合作为页面未来使用的资源或者当前页面下一跳页面要使用的资源预加载。
linkrel="prefetch"href=""linkrel="prefetch"href=""
两个标签在优先级上有一定的区别:
preload:具有高优先级,浏览器会立即加载这些资源;
prefetch:具有较低优先级,只有在浏览器空闲时才会加载这些资源,确保不妨碍当前页面的正常加载;
两者在浏览器支持上各有千秋:
五、prerer预渲染使用prerer可以将目标页面上近乎所有资源(HTML、CSS、JavaScript、图像等)和内容在后台提前下载并渲染,浏览器在用户首次访问该页面之前已经完全准备好了该页面的视图。这样当用户跳转到该页面时,使用户在实际跳转到这个页面时能够立即呈现,不需要再等待加载和渲染的时间。
linkrel="prerer"href=""
听起来prerer是预加载的终极方案了,但在实际性能优化方案中却很少被使用,使用preload有几个弊端:
不能命中时候资源开销过大:因为prerer会对页面进行资源下载和渲染,当页面没有被用户访问时候造成的资源浪费过大;
影响页面数据统计:大部分页面在执行时候会对页面进行数据上报用作后续的页面效果分析,部分页面会有展示广告等行为,如果prerer后用户没有访问页面,会造成数据统计上的混乱;
浏览器兼容性问题:不同的浏览器对于prerer的实现细节可能有所不同。例如,一些浏览器可能出于性能或安全考虑,会对预渲染的资源类型进行某些限制;
六、根据用户行为prefetch下一跳页面无脑对页面进行prefetch会造成巨大的资源浪费,但很多时候我们可以根据用户行为更精准的预测用户接下来的动作,再进行prefetch可以很大程度上减少资源浪费。
functionApp(){return(divclassName="App"h1ProductList/h1divclassName="product-list"Productid="1"name="Product1"imageUrl=""prefetchUrl="/next-page-1"/Productid="2"name="Product2"imageUrl=""prefetchUrl="/next-page-2"/Productid="3"name="Product3"imageUrl=""prefetchUrl="/next-page-3"//div/div);}constProduct=({id,name,imageUrl,prefetchUrl,delay=200})={const[prefetchTimeout,setPrefetchTimeout]=useState(null);consthandleMouseOver=()={consttimeout=setTimeout(()={constlink=('link');='prefetch';=prefetchUrl;='include';(link);},delay);//防止用户快速setPrefetchTimeout(timeout);};consthandleMouseOut=()={//如果过度发prefetch请求clearTimeout(prefetchTimeout);};return(divclassName="product"onMouseOver={handleMouseOver}onMouseOut={handleMouseOut}imgsrc={imageUrl}alt={name}loading="lazy"/p{name}/p/div);}6.1添加credentials属性,携带cookie安全原因prefetch请求默认不携带cookie,为了让prefetch请求携带cookie,可以在prefetch的link标签中添加credentials属性,并将其设置为"include"。
linkrel="prefetch"href=""as="script"credentials="include"6.2服务器设置缓存
因此需要在服务端识别prefetch请求,设置短时间的客户端缓存,当用户很快真实访问prefetch的页面后可以复用缓存。
浏览器发送的prefetch请求会携带HTTPHeaderSec-Purpose:prefetch或Purpose:prefetch,服务端根据这个属性识别prefetch请求。
('/next-page',(req,res)={constpurposeHeader=['purpose']||['sec-purpose'];if(purposeHeader==='prefetch'){('Cache-Control','max-age=10');//设置缓存策略('Prefetchrequestdetected,settingcache.');}else{('Regularrequestdetected,nocache.');}(`h1NextPageContent/h1pThisisthenextpagethatwasprefetched./p`);});七、页面与首屏请求并行加载上述方案在SSR页面效果显著,但在CSR页面可能优化效果有限,主要原因是CSR页面内容存储在CDN甚至客户端本地缓存,本身加载很快,页面的渲染主要依赖动态接口的返回。
原理非常类似,不再代码演示,核心还是请求:
设置credentials请求可以携带cookie;
服务端识别prefetch请求,对接口设置短时间的缓存;
这样的方案最大程度利用了浏览器的特性实现起来比较简单,对ServiceWorker熟悉的话可以利用Service做更复杂的控制。
八、SpeculationRulesAPISpeculationRulesAPI是一个新的WebAPI,提供一种声明式的方法来指示浏览器应该对哪些链接进行预取操作,通过这个API,开发者可以更精确地指示浏览器在何时和如何预取资源,从而显著提升网页性能和用户体验。
!DOCTYPEhtmlhtmllang="en"headmetacharset="UTF-8"titleSpeculationRulesAPI/title!--Speculationrules--scripttype="speculationrules"{"prefetch":[]}/script/headbodyahref="/"data-prefetch-url="/"GotoPage1/aahref="/"data-prefetch-url="/"GotoPage2/aahref="/"data-prefetch-url="/"GotoPage3/ascriptfunctionaddPrefetchRule(url){constspeculationRulesScript=('script[type="speculationrules"]');construles=();//检查rule是不是已经设置if(!(rule=(url))){({"source":"list","urls":[url]});//更新=(rules);(`Prefetchruleaddedfor:${url}`);}}//鼠标hover时候添加('a[data-prefetch-url]').forEach(link={('mouseover',()={consturl=('data-prefetch-url');addPrefetchRule(url);});});/script/body/htmlSpeculationRulesAPI目前还处于早期阶段,未来可能会看到更多的浏览器开始支持这一API,并且API本身也可能会引入更多的功能和配置选项。
九、页面部分内容提前返回9.1流式渲染简介流式渲染实际上一个非常古老的技术,早在规范中就已经引入了Transfer-Encoding:chunked头字段,允许服务器将响应内容分批返回给客户端。服务器可以在生成响应内容的同时,将其分成小块,逐步传输给客户端,而不是等待所有内容生成完成后再返回。
在浏览器端,早期的浏览器(如NetscapeNavigator和IE)就已经支持对部分HTML内容进行解析和执行。当浏览器接收到服务器返回的部分HTML内容时,它可以立即开始解析和执行该内容,而不需要等待所有内容加载完成。
consthttp=require('http');((req,res)={(200,{'Content-Type':'text/html','Transfer-Encoding':'chunked',});functionrerChunk(chunk){(`div${chunk}/div`);}rerChunk('Loading');setTimeout(()={rerChunk('Chunk1');},1000);setTimeout(()={rerChunk('Chunk2');},2000);setTimeout(()={rerChunk('Chunk3');},3000);setTimeout(()={rerChunk('done!');('/body/html');();},4000);}).listen(3000,()={('Serverlisteningonport3000');});9.2开启流式渲染后的新思路页面支持流式渲染之后我们可以利用等待服务器计算生成动态内容的空档,提前返回页面部分内容,在浏览器完成关键域名的预建连、核心资源的预加载,严格来讲下面讲的很多内容其实是HTTP协议实现的,但思路上和流式渲染原理一致,所以放在一块来讨论。
9.3提前返回preconnenct、preload标签页面可以对静态部分做缓存,接收到用户请求后流式渲染直接返回(其实这种最适合利用CDN边缘渲染)。
页面静态部分public/
!DOCTYPEhtmlhtmllang="en"headmetacharset="UTF-8"titleOptimizedPage/title!--Preconnecttoanexternaldomain--linkrel="preconnect"href=""linkrel="preconnect"href=""crossorigin!--PreloadacriticalCSSfile--linkrel="preload"href="/styles/"as="style"!--Preloadanimportantimage--linkrel="preload"href="/images/"as="image"/headbody
consthttp=require('http');constpath=require('path');constfs=require('fs');constfilePath=(__dirname,'public','');((req,res)={(200,{'Content-Type':'text/html','Transfer-Encoding':'chunked',});functionrerChunk(chunk){(`div${chunk}/div`);}(filePath,'utf8',(err,firstFragment)={//返回静态部分,浏览器提前建连、加载rerChunk(firstFragment);});rerChunk('Loading');setTimeout(()={//复杂的服务端计算rerChunk('done!');('/body/html');();},4000);}).listen(3000,()={('Serverlisteningonport3000');});9.4根据数据生成preloadhtml片段其实我们还可以把服务器RT部分细分,♀️页面取数部分实际非常复杂,而恰好首屏呈现的部分内容取数很快,后续取数或SSR很慢。
服务器首屏取数
服务器取数2
服务器取数3
调用页面SSR
返回服务武器渲染部分
这种时候可以在调用ssr之前,解析首屏数据生成,如果包含图片,可以生成preload标签,提前返回到浏览器,甚至对首屏部分调用占位的SSR。
consthttp=require('http');((req,res)={(200,{'Content-Type':'text/html','Transfer-Encoding':'chunked',});functionrerChunk(chunk){(`div${chunk}/div`);}setTimeout(async()={constfirstData=awaitgetFirstScreenData();rerChunk(`linkrel="preload"href="${}"as="image"`);},1000);setTimeout(()={//复杂的服务端计算rerChunk('done!');('/body/html');();},4000);}).listen(3000,()={('Serverlisteningonport3000');});用了此类技术的页面效果:
9.5httpheader返回后续域名preconenct除了在HTML中通过link标签支持preconnect,足够了解HTTP协议后我们还可以更快一些,在HTTPheader中设置preconenct,这就是HTTPLink。
HTTP/2200OKContent-Type:text/htmlLink:;;rel=preconnect,;;rel=preconnect
('/',(req,res)={//('Link',[';;rel=preconnect',';;rel=preconnect'].join(','));//();//StreamtheHTMLfileconstreadStream=('content');(res);});十、HTTP/2pushHTTP/2Push是HTTP/2协议中的一种功能,允许服务器在响应客户端请求时,主动将多个资源推送给客户端,而无需客户端明确请求这些资源。
在NGINX中,http2_push指令用于启用或禁用HTTP/2push。
server{listen443sslhttp2;server_namelocalhost;ssl_certificate/path/to/your/;ssl_certificate_key/path/to/your/;location/{root/path/to/your/web/content;;启用HTTP/2推送http2_push_preloadon;#发送HTML文档的同时,告诉客户端推送资源add_headerLink";as=image;rel=preload";}}不过通过Nginx配置来完成这个工作过于不灵活,大部分时候是通过上面讲的在服务代码中实现。
十一、EarlyHints
在HTTP1xx的状态码用来告示客户端继续进行请求或等待更详细的响应,比如在WebSocket交换协议期间返回的101。
HTTP/1.1101SwitchingProtocolsUpgrade:websocketConnection:Upgrade
还有一个专门用于服务器希望发送最终响应头部之前,提供一些消息头,客户端可以开始预加载资源的103——EarlyHints,其作用和前面提到的httplink非常类似。
HTTP/1.1103EarlyHintsLink:/;rel=preload;as=style
('/',(req,res)={//(103).set({Link:['/styles/;rel=preload;as=style','/images/;rel=preload;as=image',].join(',')}).();//SetupfinalresponseconstreadStream=('content');('Content-Type','text/html');//(res);});Earlyhintspreconnect已经在主流浏览器都得到普遍支持,preloadSafari还没有支持Web。





