加速instagram.com。 第二部分

今天,我们提请您注意instagram.com优化系列的第二篇材料的翻译。 在这里,我们将专注于改进GraphQL查询的早期执行机制,并提高将HTML数据传输到客户端的效率。



→屏息阅读, 第一部分

服务器端使用渐进HTML下载技术将数据提交给客户端


在第一部分中,我们讨论了如何使用预加载机制在页面处理的早期阶段开始执行查询。 那就是-甚至在启动此类请求的脚本加载之前。 鉴于此,可以注意到,在预加载材料阶段执行这些请求仍然意味着在客户端上呈现HTML页面之前,这些请求的执行并未开始。 反过来,这意味着请求无法在客户端向服务器发送请求并且服务器响应此请求之前开始(在这里,您还需要增加服务器生成对客户端的HTML响应所花费的时间)。 在下图中,您可以看到GraphQL查询的启动可能被相当延迟。 假设我们开始使用位于<head> HTML标记中的代码执行此类请求,并且这是我们借助数据预加载工具解决的第一批任务之一。


初步执行请求始于明显的延迟

从理论上讲,这种GraphQL查询的开始将理想地看待将加载相应页面的请求发送到服务器的那一刻。 但是,如何使浏览器甚至在从服务器接收到至少一些HTML代码之前就开始下载内容? 答案是在服务器的主动下将资源发送到浏览器。 似乎要实现这种机制,您将需要HTTP / 2 Server Push之类的东西。 但是,实际上,有一种非常古老的技术(通常被遗忘了),使您可以在客户端和服务器之间实现类似的交互方案。 该技术的特点是具有通用浏览器支持,因此无需实施HTTP / 2 Server Push的典型基础设施。 自2010年以来,Facebook就一直在使用这项技术(有关BigPipe的信息 ),在诸如Ebay之类的其他网站上,Facebook也发现了各种形式的应用程序。 但是似乎单页面应用程序的JavaScript开发人员基本上要么忽略了这项技术,要么根本不使用它。 这是关于逐步加载HTML。 这项技术以各种名称而闻名:“早期冲洗”,“头部冲洗”,“渐进式HTML”。 它的工作归功于两种机制的结合:

  • 第一个是HTTP分块传输编码。
  • 第二个是在浏览器中逐步呈现HTML。

分块传输编码机制出现在HTTP / 1.1中。 它使您可以将HTTP响应分成许多小部分,然后以流模式传输到浏览器。 浏览器“紧固”这些零件到达时,形成完整的响应代码。 尽管此方法对服务器上页面的形成方式进行了重大更改,但大多数语言和框架都可以提供类似的答案(分为多个部分)。 Instagram Web前端使用Django,因此我们使用StreamingHttpResponse对象。 使用这种机制之所以有益的原因是,它允许您在页面的各个部分准备就绪时以流模式将页面的HTML内容发送到浏览器,而不是等待整个页面代码准备就绪。 这意味着我们可以在收到请求后立即刷新浏览器的页面标题(因此,术语“早期刷新”)。 标头准备不需要特别大的服务器资源。 即使服务器忙于为页面的其余部分生成动态数据,这也使浏览器可以开始加载脚本和样式。 让我们看一下这项技术的作用。 这就是普通页面加载的样子。


不使用早期刷新技术:在页面HTML完全加载后才开始资源加载

但是,如果服务器在收到请求后立即将页面标题传递给浏览器,将会发生什么。


使用早期的刷新技术:将HTML标签转储到浏览器后,资源立即开始加载

另外,我们可以使用部分发送HTTP消息的机制,以在数据准备就绪时将数据发送到客户端。 对于在服务器上呈现的应用程序,此数据可以以HTML代码的形式呈现。 但是,如果我们谈论的是诸如instagram.com之类的单页应用程序,则服务器还可以将JSON数据之类的内容传输到客户端。 为了了解它是如何工作的,让我们看一下启动单页应用程序的最简单示例。

首先,将原始HTML标记发送到包含呈现页面所需的JavaScript代码的浏览器。 解析并执行此脚本后,将执行XHR请求,并加载呈现页面所需的源数据。


在浏览器独立地从服务器请求其需要的所有内容的情况下加载页面的过程

此过程涉及几种情况,其中客户端将请求发送到服务器,然后等待服务器的响应。 结果,有时服务器和客户端都不活动。 如果服务器在生成HTML代码后立即开始准备API响应,则不是等待服务器等待来自客户端的API请求,而是更加有效。 答案准备好后,服务器可以主动将其毒害给客户端。 这意味着,当客户端准备好可视化API请求完成后先前加载的数据所需的一切时,这些数据很可能已经准备就绪。 客户端不必满足对服务器的单独请求,也不必等待服务器的响应。

实现这种客户端-服务器交互方案的第一步是创建旨在存储服务器响应的JSON缓存。 我们使用嵌入在页面的HTML代码中的小脚本块开发了系统的这一部分。 它充当缓存的角色,并包含有关将由服务器添加到缓存的请求的信息(以下以简化形式显示)。

 <script type="text/javascript">  //      API,       ,  //     ,       ,    //        window.__data = {    '/my/api/path': {        waiting: [],    }  };  window.__dataLoaded = function(path, data) {    const cacheEntry = window.__data[path];    if (cacheEntry) {      cacheEntry.data = data;      for (var i = 0;i < cacheEntry.waiting.length; ++i) {        cacheEntry.waiting[i].resolve(cacheEntry.data);      }      cacheEntry.waiting = [];    }  }; </script> 

将HTML代码重置为浏览器后,服务器可以独立执行API请求。 收到这些请求的答案后,服务器将以包含此数据的脚本标签的形式将JSON数据转储到页面。 当浏览器接收并解析页面的HTML代码的类似片段时,这将导致数据将落入JSON缓存的事实。 这里最重要的是浏览器将逐步显示页面-当它接收到响应的片段时(也就是说,完成的脚本块将在到达浏览器时执行)。 这意味着很可能同时在服务器上同时生成大量数据,并在相应数据准备好后立即将脚本块拖放到页面上。 这些脚本将立即在客户端上执行。 这是Facebook使用的BigPipe系统的基础。 在那里,许多独立的寻呼机在服务器上并行加载,并在它们可用时传输到客户端。

 <script type="text/javascript">  window.__dataLoaded('/my/api/path', {    // JSON- API,      ,     //    JSON-...  }); </script> 

当客户端脚本准备好请求所需数据时,它首先执行JSON缓存,而不是执行XHR请求。 如果缓存中已经有查询结果,则脚本会立即收到所需的内容。 如果请求正在进行中,则脚本正在等待结果。

 function queryAPI(path) {  const cacheEntry = window.__data[path];  if (!cacheEntry) {    //   XHR-  API    return fetch(path);  } else if (cacheEntry.data) {    //          return Promise.resolve(cacheEntry.data);  } else {    //       ,    //            //       const waiting = {};    cacheEntry.waiting.push(waiting);    return new Promise((resolve) => {      waiting.resolve = resolve;    });  } } 

所有这些导致了一个事实,即页面的加载过程与下图相同。


在浏览器积极参与为客户端准备数据的情况下加载页面的过程

如果将此与最简单的页面加载方式进行比较,结果表明服务器和客户端现在可以并行执行更多任务。 这减少了服务器和客户端相互等待的停机时间。

这种优化对我们的系统产生了非常积极的影响。 因此,在桌面浏览器中,页面加载开始比以前快14%。 在移动浏览器中(由于移动网络中的延迟时间更长),页面加载速度提高了23%。

亲爱的读者们! 您是否打算在您的项目中使用该方法来优化此处讨论的网页格式?


Source: https://habr.com/ru/post/zh-CN468413/


All Articles