您的页面中可能包含不需要立即使用的组件或资源的代码或数据。例如,除非用户单击或滚动页面的一部分,否则用户不会看到部分用户界面。这可以适用于您编写的许多第一方代码,但是也适用于第三方小部件,例如视频播放器或聊天小部件,通常您需要单击按钮以显示主界面。
急于(即刻)加载这些资源会在成本高昂时阻塞主线程,从而使用户可以在多长时间内与页面的更多关键部分进行交互。这可能会影响交互就绪状态指标,例如“首次输入延迟”,“总阻止时间”和“交互时间”。您可以在更合适的时候加载它们,而不是立即加载这些资源,例如:
注意:仅当您无法在交互之前预取资源时,才应进行第一方代码的交互导入。但是,该模式与第三方代码非常相关,在第三方代码中,如果非关键时间通常需要将其延迟到以后的某个时间点。这可以通过多种方式实现(推迟直到交互,直到浏览器空闲或使用其他启发式方法)。
懒惰地在交互中导入功能代码是一种在很多情况下都将使用的模式,我们将在本文中介绍。您可能以前曾使用过它的一个地方是Google Docs,通过将其加载推迟到用户交互之前,它们可以为共享功能节省500KB的脚本加载。
您可能正在导入第三方脚本,并且对其呈现的内容或何时加载代码的控制较少。实现交互加载的一种选择是直接的:使用外观。外观是简单的" Preview"或" placeholder"用于模拟基础体验(例如图像或屏幕截图)的成本更高的组件。我们在Lighthouse团队中一直使用这个术语来表达这个想法。
当用户点击“预览”时(外观),将加载资源的代码。这限制了用户在不使用某个功能时需要支付体验费用。同样,外观可以在悬停时预先连接到必要的资源。
注意:第三方资源通常添加到页面中,而没有充分考虑它们如何适合网站的整体加载。同步加载的第三方脚本会阻止浏览器解析器,并可能延迟水化。如果可能,应使用异步/延迟(或其他方法)加载3P脚本,以确保1P脚本不会出现网络带宽不足的情况。除非它们很关键,否则它们可能是使用交互导入等模式转移到延迟后加载的一个很好的选择。
幕墙的一个很好的例子是Paul Irish嵌入的YouTube Lite。这提供了一个自定义元素,该元素带有YouTube视频ID,并显示最小的缩略图和播放按钮。点击该元素会动态加载完整的YouTube嵌入代码,这意味着从不单击播放的用户无需支付获取和处理它的费用。
在一些Google网站上的生产中也使用了类似的技术。在Android.com上,用户不会急于加载嵌入的YouTube视频播放器,而是向用户显示带有伪造的播放器按钮的缩略图。当他们单击它时,将加载模态,该模态将使用嵌入的全脂YouTube视频播放器自动播放视频:
应用可能需要通过客户端JavaScript SDK支持服务的身份验证。这些有时可能会很大,而JS执行成本却很高,如果用户不打算登录,我宁愿不急于将它们预先加载。相反,当用户单击“登录”时,我会动态导入身份验证库。按钮,使主线程在初始加载期间更加自由。
通过使用类似的外观方法,Caliber应用程序将基于对讲的实时聊天的性能提高了30%。他们实施了“伪造”仅使用CSS和HTML快速加载实时聊天按钮,单击该按钮即可加载其对讲包。
邮戳指出,即使只是偶尔由客户使用,他们的“帮助”聊天窗口小部件始终总是急切加载。该小部件将提取314KB脚本,比其整个主页还要多。为了提高用户体验,他们使用HTML和CSS将小部件替换为伪造的副本,并在单击时加载真实内容。此更改将“交互”的时间从7.7s减少到3.7s。
当用户单击“滚动到顶部”时,Ne-digital使用React库进行动画滚动回到页面顶部。按钮。他们与其急于加载react-scroll依赖关系,而是在与按钮交互时加载了它,节省了约7KB:
在JavaScript中,动态import()启用延迟加载模块并返回Promise,并且在正确应用时功能非常强大。下面是一个示例,其中在按钮事件侦听器中使用动态导入来导入lodash.sortby模块,然后使用它。
const btn =文档。 querySelector(' button'); btn。 addEventListener(' click',e => {e。preventDefault(); import(' lodash.sortby')。然后(module => module.default)。然后(sortInput ())//使用导入的依赖项catch(err => {console。log(err)});});
在动态导入或不适合用例之前,我将使用基于Promise的脚本加载器在页面中动态注入脚本(有关演示登录外观的完整实现,请参见此处):
const loginBtn = document。 querySelector('#login'); loginBtn。 addEventListener(' click',()=> {const loader = new scriptLoader(); loader。load([' // apis.google.com/js/client:platform.js? onload = showLoginScreen'])。然后(({length})=> {console。log(`$ {length}脚本已加载!`);});});
假设我们有一个聊天应用程序,其中有一个< MessageList>,< MessageInput>和< EmojiPicker>组件(由emoji-mart提供支持,最小化并压缩了98KB)。急于在初始页面加载时加载所有这些组件是很常见的。
从' ./ MessageList'导入MessageList ;从' ./ MessageInput'导入MessageInput ;从' ./ EmojiPicker'导入EmojiPicker ; const Channel =()=> {... return(< div>< MessageList />< MessageInput /> {emojiPickerOpen&< EmojiPicker />}< / div>)); };
通过代码拆分,可以很轻松地分解工作量。使用React.lazy方法可以轻松地使用动态导入在组件级别上对React应用程序进行代码拆分。 React.lazy函数提供了一种内置的方法,只需很少的工作即可将应用程序中的组件分离为JavaScript的单独块。然后,将其与Suspense组件结合使用时,您便可以处理加载状态。
从' react'导入React,{lazy,Suspense} ;从' ./ MessageList'导入MessageList ;从' ./ MessageInput'导入MessageInput ; const EmojiPicker = lazy(()=> import(' ./ EmojiPicker')); const Channel =()=> {... return(< div>< MessageList />< MessageInput /> {emojiPickerOpen&&((< Suspense fallback = {< div> Loading ...< / div> }>< EmojiPicker />< / Suspense>)}< / div>); };
我们可以将此思想扩展为仅在< MessageInput>中单击Emoji图标时才为Emoji Picker组件导入代码,而不是在应用程序最初加载时急切地导入:
从' react'导入React,{useState,createElement} ;从' ./ MessageList'导入MessageList ;从' ./ MessageInput'导入MessageInput ;从' ./ ErrorBoundary'导入ErrorBoundary。 ; const Channel =()=> {const [emojiPickerEl,setEmojiPickerEl] = useState(null); const openEmojiPicker =()=> {import(/ * webpackChunkName:" emoji-picker" * /' ./ EmojiPicker')。然后(module => module.default)。然后(emojiPicker => {setEmojiPickerEl(createElement(emojiPicker));}); }; const closeEmojiPickerHandler =()=> {setEmojiPickerEl(null); }; return(< ErrorBoundary>< div>< MessageList />< MessageInput onClick = {openEmojiPicker} /> {emojiPickerEl}< / div>< / ErrorBoundary>); };
在Vue.js中,可以通过几种不同的方式来实现类似的交互导入模式。一种方法是使用包装在函数中的动态导入来动态导入Emojipicker Vue组件,即()=> import(" ./ Emojipicker")。通常,需要渲染时,Vue.js会延迟加载组件。
然后,我们可以控制用户交互背后的延迟加载。通过在选择器的父div上使用条件v-if(可通过单击按钮进行切换)来实现,然后我们可以在用户单击时有条件地获取并渲染Emojipicker组件。
<模板> < div> < button @ click =" show = true" >加载表情符号选择器< / button> < div v-if =" show" > < emojipicker>< / emojipicker> < / div> < / div> < / template> < script>导出默认值{data:()=> ({show:false}),组件:{Emojipicker:()=>导入(&#39 ../ Emojipicker')}}; < / script>
交互导入模式在大多数支持动态组件加载的框架和库(包括Angular)中应该可行。
在交互中加载代码也恰巧是Google如何处理Flight and Photos等大型应用程序中的渐进式加载的关键部分。为了说明这一点,让我们看一下Shubhie Panicker先前提供的示例。
假设有一个用户计划前往印度孟买旅行,然后他们访问Google Hotels查看价格。这种互动所需的所有资源都可以预先加载,但是如果用户未选择任何目的地,则地图不需要HTML / CSS / JS。
在最简单的下载方案中,想象Google Hotels正在使用幼稚的客户端渲染(CSR)。所有代码都将被预先下载和处理:HTML,然后是JS,CSS,然后获取数据,只有在我们拥有所有内容后才进行渲染。但是,这会使用户等待很长时间,而屏幕上没有任何显示。大量的JavaScript和CSS可能是不必要的。
接下来,想象一下这种体验已转移到服务器端渲染(SSR)。我们将允许用户更快地获得视觉上完整的页面,这是很棒的,但是直到从服务器获取数据并且客户端框架完成水合作用,该页面才是交互式的。
SSR可能是一种改进,但用户可能会在页面看上去已准备就绪的情况下体验到令人难以置信的低谷体验,但他们无法轻敲任何东西。有时这被称为“愤怒点击”,因为用户往往会沮丧地反复点击一遍。
回到Google Hotels搜索示例,如果我们稍微放大一下UI,我们可以看到,当用户点击" more过滤器"为了找到正确的酒店,需要下载该组件所需的代码。
最初只下载非常少的代码,除此之外,用户交互作用指示何时发送哪个代码。
接下来,当用户开始与页面进行交互时,我们将使用这些交互来确定要加载的其他代码。例如,加载" more过滤器"的代码零件。
这意味着页面上许多功能的代码永远不会发送到浏览器,因为用户不需要使用它们。
在这些Google团队使用的框架堆栈中,我们可以尽早跟踪点击,因为HTML的第一块包含一个小型事件库(JSAction),该事件库在启动框架之前跟踪所有点击。该事件用于两件事:
基于浏览器信号(例如网络速度,数据保护程序模式等)的急切程度。
用于呈现页面的初始数据包含在初始页面的SSR HTML中并进行流式处理。延迟加载的数据是根据用户的交互进行下载的,因为我们知道它包含什么组件。
这样就完成了交互导入图片,其数据获取工作类似于CSS和JS的功能。当组件知道它需要什么代码和数据时,它的所有资源都不过是一个请求而已。
当我们在构建期间创建组件及其依赖关系图时,此功能起作用。 Web应用程序可以随时参考该图,并快速获取任何组件所需的资源(代码和数据)。这也意味着我们根据组件而不是路线进行代码分割。
有关以上示例的演练,请参阅使用JavaScript社区提升Web平台。
将昂贵的工作转移到更接近用户交互的位置可以优化页面初始加载的速度,但是该技术并非没有取舍。
如果用户单击后加载脚本需要很长时间,该怎么办?
在Google Hotels的示例中,细小的块将用户长时间等待代码和数据获取和执行的机会降到最低。在某些其他情况下,很大的依赖关系确实可能在较慢的网络上引起这种担忧。
减少这种情况发生的可能性的一种方法是,在页面中的关键内容完成加载后,更好地分解或预取这些资源。我鼓励您评估此影响,以确定实际应用中有多少个应用。
与立面的另一个权衡是在用户交互之前缺少功能。例如,嵌入式视频播放器将无法自动播放媒体。如果此类功能很关键,则可以考虑使用替代方法来加载资源,例如,在用户上延迟加载这些第三方iframe,将其滚动到视图中,而不是将加载推迟到交互之前。
我们已经讨论了交互导入模式和渐进式加载,但是对于嵌入用例而言,如何完全静态化呢?
在某些情况下,例如在初始视口中可见的社交媒体帖子中,可能需要立即使用嵌入内容的最终渲染内容。当嵌入带来2-3MB的JavaScript时,这也可能带来自身的挑战。由于立即需要嵌入内容,因此延迟加载和外观可能不太适用。
如果要针对性能进行优化,则可以用看起来相似的静态变体完全替换嵌入内容,从而链接到更具交互性的版本(例如原始社交媒体帖子)。在构建时,可以提取嵌入的数据并将其转换为静态HTML版本。
这是@wongmjane在其博客上利用的一种社交媒体嵌入方法,既可以改善页面加载性能,又可以消除由于嵌入代码增强后备文本而导致的累积版式移位,从而导致版式移位。
尽管静态替换可以提高性能,但它们经常需要做一些自定义操作,因此在评估选项时请记住这一点。
第一方JavaScript通常会影响Web上现代页面的交互就绪性,但是在非关键JS之后的网络中,第一方JavaScript经常会由于来自第一方或第三方的,使主线程忙碌而延迟。
通常,请避免在文档头中使用同步的第三方脚本,并旨在在第一方JS完成加载后加载非阻塞的第三方脚本。诸如交互导入(Import-on-Interaction)之类的模式为我们提供了一种将非关键资源的加载推迟到用户更可能需要其提供动力的UI的方式。
特别感谢Shubhie Panicker,Connor Clark,Patrick Hulce,Anton Karlovskiy和Adam Raine的投入。