正如在上一篇关于 渲染引擎 的博客文章中提到的,我们认为优秀的 JavaScript 开发人员和杰出的 JavaScript 开发人员之间的区别在于,后者不仅理解语言的具体细节,而且理解其内部结构和周遭环境。
讲一点历史
49年前,一种叫做 ARPAnet 的网诞生了。它是一个早期的 分组交换网络,也是第一个 实现TCP/IP套件的网络。20年后,蒂姆·伯纳斯-李提出了一种“网状结构”的建议,这种结构后来被称为“万维网”。在这 49 年里,互联网走过了漫长的道路,从仅仅两台计算机交换数据包,到超过 7500
万台服务器、38
亿互联网用户和 13
亿个网站。
"阿帕"(ARPA),是美国高级研究计划署(Advanced Research ProjectAgency)的简称。他的核心机构之一是信息处理(IPTO Information Processing Techniques Office),一直在关注电脑图形、网络通讯、超级计算机等研究课题。
阿帕网为美国国防部高级研究计划署开发的世界上第一个运营的封包交换网络,它是全球互联网的始祖。
在这篇文章中,我们将尝试分析现代浏览器使用什么技术来自动提高性能(甚至在你不知道的情况下),接着深入浏览器网络层。最后,我们将提供一些关于如何帮助浏览器提高 Web 应用程序性能的建议。
概览
现代 Web 浏览器专为快速,高效,安全地提供网络应用/网站而设计。 数百个组件在不同的层上运行,从流程管理和安全沙箱到 GPU 管道,音频和视频等等,Web 浏览器看起来更像是一个操作系统,而不仅仅是一个软件应用程序。
浏览器的总体性能由许多大型组件决定:解析、布局、样式计算、JavaScript 和 WebAssembly 执行、渲染,当然还有网络堆栈。
工程师经常认为网络堆栈是一个瓶颈。这种情况经常发生,因为所有资源都需要从网上获取,然后才能解除其余步骤的阻塞。为了使网络层高效,它需要扮演的角色不仅仅是一个简单的套接字管理器。它提供给我们的是一种非常简单的资源获取机制,但实际上它是一个具有自己的优化标准、API 和服务的完整平台。
作为 Web 开发人员,我们不必担心单独的 TCP 或 UDP 数据包、请求格式化、缓存和其他一切问题。整个复杂性由浏览器负责,因此我们可以将精力集中在我们正在开发的应用程序上。然而,了解底层的情况可以帮助我们创建更快、更安全的应用程序。
本质上,当用户开始与浏览器交互时会发生以下情况:
用户在浏览器地址栏中输入一个 URL
给定 Web 上资源的 URL,浏览器首先检查其本地缓存和应用程序缓存,并尝试使用本地副本来完成请求
如果缓存不能使用,浏览器从 URL 获取域名,并从 DNS 请求服务器的 IP 地址。如果域被缓存,则不需要 DNS 查询
浏览器创建一个 HTTP 包,表示它请求位于远程服务器上的 Web 页面
数据包被发送到 TCP 层,TCP 层在 HTTP 数据包上添加自己的信息,维护已启动的会话需要此信息
然后数据包被传递给 IP 层,IP 层的主要任务是找出一种将数据包从用户发送到远程服务器的方法,这些信息也存储在包的顶部
数据包被发送到远程服务器
一远程服务器一旦接收到数据包,就会以类似的方式发回响应。
W3C的浏览时序规范(Navigation Timing specification)提供了一个浏览器API,让我们可以看到浏览器中每项请求的生命周期背后的时序和性能数据。让我们看看这些组成部分,每一块都是影响最佳用户体验的关键点:
整个网络过程非常复杂,有许多不同的层,这可能成为瓶颈。这就是为什么浏览器努力通过使用各种技术来提高自己的性能,从而使整个网络通信的影响最小。
套接字管理
先了解一些术语:
源(Origin) - 由应用程序协议,域名和端口号组成(例如https,www.example.com,443)
套接字池(Socket pool) - 属于同一源的一组套接字(所有主要浏览器将最大池大小限制为6个套接字)
JavaScript 和 WebAssembly 不允许我们管理单个网络套接字的生命周期,这是一件好事!这不仅使我们的省去较多麻烦,而且还可以让浏览器自动进行许多性能优化,其中包括套接字重用、请求优先级和后期绑定、协议协商、强制连接限制等。
实际上,现代浏览器在将请求管理周期与套接字管理分离方面做了更多的工作。套接字组织在按源分组的池中,每个池执行自己的连接限制和安全约束。挂起的请求被排队、排序,然后绑定到池中的各个套接字。除非服务器有意关闭连接,否则同一个套接字可以跨多个请求自动重用!
由于打开新的 TCP 连接需要额外的成本,因此连接的重用本身就带来了巨大的性能优势。默认情况下,浏览器使用所谓的 “keepalive” 机制,它可以在发出请求时节省打开到服务器的新连接的时间。打开新 TCP 连接的平均时间为:
当地的请求 —
23ms
横贯大陆的请求 ——
120ms
洲际请求 ——
225ms
这种架构为其他一些优化提供了可能, 请求可以根据其优先级以不同的顺序执行。 浏览器可以优化所有套接字的带宽分配,也可以在预期请求时打开套接字。
正如之前提到的,这一切都由浏览器管理,不需要我们做任何工作,但这并不意味着我们什么都做不了。 选择正确的网络通信模式,类型和传输频率,协议选择以及服务器堆栈的调优/优化可以在提高应用程序的整体性能方面发挥重要作用。
有些浏览器甚至更进了一步。 例如,Chrome 可以学习用户的操作习惯来使自己变得更快。 它根据访问的站点和典型的浏览模式进行学习,以便预测可能的用户行为并在用户执行任何操作之前采取措施。 最简单的例子是当用户在链接上悬停时,Chrome 会预先渲染页面, 如果有兴趣了解有关 Chrome 优化的更多信息,可以查看这篇文章 https://www.igvita.com/posa/high-performance-networking-in-google-chrome
网络安全和沙盒
允许浏览器管理单个套接字还有另一个非常重要的目的:通过这种方式,浏览器能够对不受信任的应用程序资源执行一致的安全和策略约束。例如,浏览器不允许 API 直接访问原始网络套接字,因为这将使任何恶意应用程序能够任意连接到任何主机。浏览器还强制执行连接限制,以保护服务器和客户端免于资源耗尽。
浏览器格式化所有传出请求,以强制执行一致且格式良好的协议语义,以保护服务器。类似地,响应解码是自动完成的,以保护用户免受恶意服务器的攻击。
TLS 协议
传输层安全性协议 (Transport Layer Security, TLS)是一种通过计算机网络提供通信安全性的加密协议。它在许多应用程序中得到了广泛的应用,其中之一就是 Web 浏览器。网站可以使用 TLS 保护服务器和Web 浏览器之间的所有通信。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面。
整个TLS握手包括以下步骤:
客户端向服务器发送
“Client hello”
消息,与之一同发送的还有客户端产生的随机值和支持的密码套件。服务器通过向客户端发送 “Server hello” 消息及服务器产生的随机值进行响应。
服务器将其证书发送给客户端,并可以从客户端请求类似的证书。 服务器发送
“Server hello done”
消息。如果服务器向客户机请求了证书,客户机将发送证书。
客户端创建一个随机的
Pre-Master Secret
,并使用服务器证书中的公钥对其进行加密,将加密的 Pre-Master Secret 发送到服务器。服务器接收
Pre-Master Secret
。 服务器和客户端均基于预主密钥生成主密钥和会话密钥。客户端向服务器发送
“Change cipher spec”
通知,以指示客户端将开始使用新的会话密钥进行散列和加密消息。 客户端还发送“Server finished”
消息。服务器接收
“Change cipher spec”
,并使用会话密钥将其记录层安全状态切换为对称加密。 服务器向客户端发送“Server finished”
消息。客户端和服务器现在可以通过他们已建立的安全通道交换应用程序数据。 从客户端发送到服务器并返回的所有消息都使用会话密钥加密。
如果任何验证失败,则警告用户 - 例如,服务器正在使用自签名证书。
同源策略(same-origin policy)
同源是指文档的来源相同,主要包括三个方面
协议
主机
载入文档的 URL 端口
以下是一些可能嵌入跨源资源的一些例子:
带有
<script src =“...”> </ script>
的 JavaScript。 语法错误的错误消息仅适用于同源脚本带有
<link rel =“stylesheet”href =“...”>
的CSS。 由于 CSS 的宽松语法规则,跨源 CSS 需要正确的 Content-Type 标头。不同浏览器可能有不同的限制通过
<img>
加载图片带有
<video>
和<audio>
的媒体文件带有
<object>
,<embed>
和<applet>
的插件带 @font-face 的字体。 某些浏览器允许跨源字体,其他浏览器需要同源字体
任何有
<frame>
和<iframe>
的东西。 站点可以使用 X-Frame-Options 头部标识来阻止这种形式的跨源交互
以上列表并非完整,其目的是强调工作中 “最小特权” 的原则。 浏览器仅公开应用程序代码所需的 API 和资源:应用程序提供数据和 URL,浏览器格式化请求并处理每个连接的整个生命周期。
值得注意的是,**“同源策略”**并不是一个单一概念。相反,有一组相关的机制来限制对 DOM 访问、cookie 和会话状态管理、网络和浏览器的其他组件。
资源和客户端状态缓存
最佳请求是没有重新请求。在发送请求之前,浏览器会自动检查其资源缓存,执行必要的验证检查,并在满足指定条件的情况下返回资源的本地副本。如果缓存中没有可用的本地资源,则发出网络请求,并自动将响应放置在缓存中,以便在有权限的情况下进行后续访问。
浏览器自动评估每个资源上的缓存指令
浏览器会尽可能自动重新验证过期资源
浏览器自动管理缓存大小和资源回收
管理高效且优化的资源缓存很难。 值得庆幸的是,浏览器帮我们处理整个复杂事情,我们需要做的就是确保我们的服务器返回适当的缓存指令; 要了解更多信息,请参阅 客户端的缓存资源(Cache Resources on the Client)。 这个需要我们为页面上的所有资源提供了 Cache-Control,**ETag ** 和 Last-Modified 响应头部标志。
最后,浏览器的一个经常被忽视的关键功能是提供身份验证、会话和 cookie 管理。浏览器为每个源维护独立的 “cookie jars”,提供必要的应用程序和服务器 Api 来读写新的 cookie、会话和身份验证数据,并自动附加上和处理相应的 HTTP 头以代替我们自动执行整个过程。
来个例子:
用一个简单但有说明性的例子来说明将会话状态管理推放到浏览器端的便利之处:同一个经过身份验证的会话可以在多个选项卡或浏览器窗口之间共享,反之亦然;单个选项卡中的注销操作将使所有其他打开的窗口中打开的会话失效。
应用程序 Api 和协议
研究完了网络服务,终于到达了应用程序 API 和协议这一步。正如我们所看到的,底层提供了大量关键服务:套接字和连接管理、请求和响应处理、各种安全策略的执行、缓存等等。每当我们启动 HTTP 或 XMLHttpRequest 、长期的 Server-Sent Events
或 WebSocket 会话,或打开 WebRTC 连接时,我们都在与这些底层服务进行交互。
没有单一的最佳协议或 API。 每个稍微复杂的应用程序都需要根据各种要求混合使用不同的传输:与浏览器缓存的交互,协议开销,消息延迟,可靠性,数据传输类型等。 某些协议可能提供低延迟传送(例如,Server-Sent Events,WebSocket),但可能不符合其他关键标准,例如在所有情况下利用浏览器缓存或支持有效二进制传输的能力。
以下是一些来提高 Web 应用程序的性能和安全性技巧
始终在请求中使用 “Connection: Keep-Alive” 头部标识,浏览器默认执行此操作,确保服务器使用相同的机制。
使用正确的 Cache-Control,Etag 和 Last-Modified 头部标识,这样就可以节省浏览器的下载时间。
花时间调整和优化你的 Web 服务器,这是才是真正的最有效的地方! 请记住,该过程要针对每个 Web 应用程序以及你要传输的数据的类型要更加具体考虑和处理。
始终使用TLS,特别是如果你的应用程序中有任何类型的身份验证。
研究浏览器在你的应用程序中提供和实施的安全策略。
原文: