[译]谷歌Web性能优化系列:HTTP 请求(中英)

3,121 阅读17分钟

原文链接(需越墙)developers.google.com/web/fundame…

原文作者:Dave Gash 译者西楼听雨

译注:此作是谷歌开发者网站关于Web性能优化的入门系列文章之一,该系列的其他篇章,及对应的高级系列文章,本人后续会持续发布,欢迎大家关注。(转载请注明出处)

Everything a web page needs to be a web page -- text, graphics, styles, scripts, everything -- must be downloaded from a server via an HTTP request. It's no stretch to say that the vast majority of a page's total display time is spent in downloading its components, not in actually displaying them. So far, we've talked about reducing the size of those downloads by compressing images, minifying CSS and JavaScript, zipping the files, and so on. But there's a more fundamental approach: in addition to just reducing download size, let's also consider reducing download frequency.

一张网页所需要的所有东西——文本,图像,样式,脚本,等等——一定都是通过HTTP请求下载下来的。无可辩驳地说,呈现一张网页的耗时绝大多数都是用在下载这些部件的过程中,而不是实际展示他们所需的时间。目前为止,我们讨论减少下载这些文件的体积,都是在说如何压缩图片,紧凑(minification) CSS 和 Javascript ,并打包(zipping)这些文件,等等。但实际上还有更接近底层的方式:除了减小下载大小,我们还可以考虑降低下载次数。

Reducing the number of components that a page requires proportionally reduces the number of HTTP requests it has to make. This doesn't mean omitting content, it just means structuring it more efficiently.

减少一张网页所需的部件数量,可以适当地减少HTTP的请求次数。但这是并不是说让你丢弃一些内容,而是以更高效的方式来编排这些部件。

合并文本资源(Combine Text Resources)

Many web pages use multiple stylesheets and script files. As we develop pages and add useful formatting and behavior code to them, we often put pieces of the code into separate files for clarity and ease of maintenance. Or we might keep our own stylesheet separate from the one the Marketing Department requires us to use. Or we might put experimental rules or scripts in separate files during testing. Those are all valid reasons to have multiple resource files.

现在的许多网页都会使用多个样式文件和脚本文件。随着我们不断地的开发页面并往里面添加需要的样式和行为代码,我们经常会将一些代码块放在一些单独的文件中,以确保代码清晰便于维护。比如,我们会将自己的样式文件和市场部要求的文件单独隔开;在测试的过程中,会将一些实验性的样式规则或者脚本隔开。这些做法对于使用多个样式文件来说都是没错的。

But because each file requires its own HTTP request, and each request takes time, we can speed up page load by combining files; one request instead of three or four will certainly save time. At first blush, this seems like a no-brainer -- just put all the CSS (for example) into a master stylesheet and then remove all but one from the page. Every extra file you eliminate in this way removes one HTTP request and saves round-trip time. But there are caveats.

但是由于每个文件都需要有自己的请求,而每次请求又需要一定的时间,因此我们可以将文件合并来加速页面的加载;一次请求而不是三四次请求,明显地节约时间。虽然乍一看,这种操作显得很无知——将所有CSS(这里只是举个例子,也可以是其他文件)合并为一个大的样式文件并将其他文件从页面中移除。但是每个被移除的文件都可以减少一次请求,进而减少服务器来回时间。尽管如此,还是有一些需要注意的地方。

For Cascading Style Sheets, beware the "C". Cascade precedence allows later rules to override earlier ones without warning -- literally. CSS doesn't throw an error when a previously-defined property is reset by a more recent rule, so just tossing stylesheets together is asking for trouble. Instead, look for conflicting rules and determine whether one should always supercede the other, or if one should use more specific selectors to be applied properly. For example, consider these two simple rules, the first from a master stylesheet, the second imported from a stylesheet provided by Marketing.

对于“层叠样式表”来说,要特别注意“层叠”。层叠的特性,会导致位于后面的样式规则在无任何提示的情况下覆盖掉前面的样式规则。另外,当定义在前面的属性被重置的时候,CSS也不会抛出任何错误。所以,仅仅是简单地将样式文件合并到一起是会引起一些麻烦的。除此之外,我们还需要查找样式规则的冲突,并决定他们之间的顺序,或者判断某个样式规则是否应该使用更限定化的选择器,以确保他们正常。例如下面代码中的两条简单的样式规则,第一条来自主样式表文件,第二条是从市场部提供的样式表文件中导入的。

h2 { font-size: 1em; color: #000080; } /* master stylesheet */

. . .

h2 { font-size: 2em; color: #ff0000; } /* Marketing stylesheet */

Marketing wants their h2s to be prominent, while yours are meant to be more subdued. But due to the cascading order, their rule takes precedence and every h2 in the page will be big and red. And clearly, you can't just flip the order of the stylesheets or you'll have the same problem in reverse.

市场部希望他们的h2们突出显示,而你的则普通暗淡。但是因为层叠顺序的原因,他们的规则获得了更高的优先级,这样页面上的每个h2元素都会变大变成红色。而且,你也无法仅仅通过调换这些文件的顺序就解决问题。

A little research might show that Marketing's h2s always appear inside a specific class of section, so a tweak to the second rule's selector resolves the conflict. Marketing's splashy h2s will still look exactly as they want, but without affecting h2s elsewhere in the page.

稍微做一点研究,你就会发现,市场部的h2们总是位于某一个固定的 class 块中,所以我们对第二条规则的选择器做一点微调即可解决这个冲突。这样,市场部的h2们与之前他们想要的效果一样,同时又不影响到页面中其他地方的h2们。

h2 { font-size: 1em; color: #000080; }

. . .

section.product h2 { font-size: 2em; color: #ff0000; }

You may run into similar situations when combining JavaScript files. Completely different functions might have the same names, or identically-named variables might have different scopes and uses. These are not insurmountable obstacles if you actively look for them.

不过,即便在合并的是 JavaScript 文件,你也可能会遇到同样的问题。功能风马牛不相及的函数可能有着相同的名称;相同名字的变量却有着不同的scope和用处。如果你认真排除,其实这些都不是无法避免的障碍。

Combining text resources to reduce HTTP requests is worth doing, but take care when doing so. See A Caveat below.

合并文本类型的资源可以减少HTTP请求,这是值得去做的事情,但在做的过程中也需要额外留意。请查看后文中的“一个注意点”一节。

合并图像资源(Combine Graphical Resources)

On its face, this technique sounds a bit nonsensical. Sure, it's logical to combine multiple CSS or JavaScript resources into one file, but images? Actually, it is fairly simple, and it has the same effect of reducing the number of HTTP requests as combining text resources -- sometimes even more dramatically.

表面看来,这种技术听上去有点不现实。对于将多个 CSS 或 JavaScript 资源合并为一个资源逻辑上当然是合理的,但图片呢?实际上,图片的合并也非常简单,而且和文本资源的合并有同样的效果——有时效果甚至更强烈。

Although this technique can be applied to any group of images, it is most frequently used with small images such as icons, where extra HTTP requests to fetch multiple small graphics are particularly wasteful.

虽然这种技术可以应用到任何一组图片,但大多数时候更常被用于小尺寸的图片,例如图标,(因为在这种情形下)花费额外的请求来获取多个这样的小尺寸图像是非常浪费的。

The basic idea is to combine small images into one physical image file and then use CSS background positioning to display only the right part of the image -- commonly called a sprite -- at the right place on the page. The CSS repositioning is fast and seamless, works on an already-downloaded resource, and makes an excellent tradeoff for the otherwise-required multiple HTTP requests and image downloads.

这种技术的基本思路是,将小尺寸图片在物理上拼成一个图片文件,然后利用CSS的背景定位特性将相应需要展示的部分展示在页面的相应位置上——这通常称为“一个sprite”。CSS的定位特性不仅速度快而且效果自然,可以非常有效地抵消HTTP请求和图片下载次数。

For example, you might have a series of social media icons with links to their respective sites or apps. Rather than downloading three (or perhaps many more) individual images, you might combine them into one image file, like this.

举个例子,假设你有一组社交网站的图标,每个图标链接到他们各自对应的网站或者应用。相对于通过三次(或许更多)请求来下载每个图标,(现在)你可能会选择将他们合并为一个图片文件,像这样:

Then, instead of using different images for the links, just retrieve the entire image once and use CSS background positioning ("spriting") for each link to display the correct part of the image for the link.

然后,仅仅通过一次请求获取整体图片,并利用CSS的背景定位(spriting)来确保每个链接正确展示整体图片中的相应的部分。

Here's some sample CSS.

下面是一段CSS示例代码:

a.facebook {
   display: inline-block;
   width: 64px; height: 64px;
   background-image: url("socialmediaicons.png");
   background-position: 0px 0px;
   }
a.twitter {
   display: inline-block;
   width: 64px; height: 64px;
   background-image: url("socialmediaicons.png");
   background-position: -64px 0px;
   }
a.pinterest {
   display: inline-block;
   width: 64px; height: 64px;
   background-image: url("socialmediaicons.png");
   background-position: -128px 0px;
   }

Note the extraneous background-position property in the facebook class. It's not necessary, as the default position is 0,0 but is included here for consistency. The other two classes simply shift the image left horizontally relative to its container by 64px and 128px, respectively, leaving the appropriate image section visible in the 64-by-64 pixel link "window".

注意,上面代码中的 facebook 类中的backgournd-position属性,其实并不是必须的,因为positon属性的默认值就是0,0,在这把它写出来是为了一致性的原因。另外两个类则分别相对于包含他们的容器进行了64px128px的水平位移,以确保链接在图片中的对应部分得以以64X64像素的大小正确显示。

Here's some HTML to use with that CSS.

下面是一段使用了上面CSS代码的HTML示例代码:

<p>Find us on:</p>
<p><a class="facebook" href="https://facebook.com"></a></p>
<p><a class="twitter" href="https://twitter.com"></a></p>
<p><a class="pinterest" href="https://pinterest.com"></a></p>

Instead of including separate images in the links themselves, just apply the CSS classes and leave the link content empty. This simple technique saves you two HTTP requests by letting CSS do the image shifting behind the scenes. If you have lots of small images -- navigation icons or function buttons, for example -- it could save you many, many trips to the server.

上面的代码,不是分别将图片置于每个链接标签之间,而是仅仅通过应用 CSS 类并留空链接的内容来实现相同的效果。这种技巧利用了 CSS 来做图片位移,减少了两次HTTP请求。当你有大量小尺寸图片时——导航菜单图标、操作按钮——这会减少许多,许多和服务器交互的来回。

You can find a brief but excellent article about this technique, including working examples, at WellStyled.

Wellstyled 上,有一篇关于这种技术的简单但非常棒的文章,还包含了一些可用的示例,你可以查阅下。

一个注意点(A Caveat)

In our discussion of combining text and graphics, we should note that the newer HTTP/2 protocol may change how you consider combining resources. For example, common and valuable techniques like minification, server compression, and image optimization should be continued on HTTP/2. However, physically combining files as discussed above might not achieve the desired result on HTTP/2.

在上面合并文本和图像的讨论中,我们应该留意的一点是,新的HTTP/2协议可能会改变我们对资源合并的想法。例如,常见的和有价值的技术,像代码紧凑(minification),服务端压缩,和图片优化等都应该继续保留。但是,前面我们讨论的,从物理上对文件进行合并则可能并不会达到我们想要的结果。

This is primarily because server requests are faster on HTTP/2, so combining files to eliminate a request may not be substantially productive. Also, if you combine a fairly static resource with a fairly dynamic one to save a request, you may adversely affect your caching effectiveness by forcing the static portion of the resource to be reloaded just to fetch the dynamic portion.

这主要是因为在HTTP/2协议里,请求服务器的效率变得更高效了,所以以合并文件来减少请求可能并没有什么特别的效果。同样,如果将相当数量的静态资源合并为一个相当动态的资源,以此来减少请求,相反,你可能还会影响到缓存的生效,因为这会导致,为获取动态的部分而强制刷新静态的部分的问题。

The features and benefits of HTTP/2 are worth exploring in this context.

HTTP/2的这些特性和益处,在本文的主题中,都是值得进行一翻探究的。

JavaScript 的位置和内联推入 (JavaScript Position and Inline Push)

We're assuming so far that all CSS and JavaScript resources exist in external files, and that's generally the best way to deliver them. Bear in mind that script loading is a large and complex issue -- see this great HTML5Rocks article, Deep Dive Into the Murky Waters of Script Loading, for a full treatment. There are, however, two fairly straightforward positional factors about JavaScript that are worth considering.

目前我们都是假设所有 CSS 和 JavaScript 都是外部资源,这也是传输他们的最好方式。不过请记住,脚本的加载是一个大而又复杂的问题——可以参考 HTML5Rocks 上的这篇不错的文章,Deep Dive Into the Murky Waters of Script Loading。下面是两种值得思考的,“位置影响”的直接因素。

脚本位置(Script Location)

Common convention is to put script blocks in the page head. The problem with this positioning is that, typically, little to none of the script is really meant to execute until the page is displayed but, while it is loading, it unnecessarily blocks page rendering. Identifying render-blocking script is one of the reporting rules of PageSpeed Insights.

通常我们是将脚本块放置到页面的头部(head)。这种脚本置放方式有一个问题,通常,几乎没有那个脚本是真实需要在页面加载且并未显示时就执行的,这就导致页面渲染会被阻塞。也正因为如此,鉴别阻塞性代码也是PageSpeed Insights工具设定报表规则中的其中一项。

A simple and effective solution is to reposition the deferred script block at the end of the page. That is, put the script reference last, just before the closing body tag. This allows the browser to load and render the page content, and then lets it download the script while the user perceives the initial content. For example:

解决这个问题的一种简单而又有效的方法是,将这些可以延后执行的脚本块移至页面的尾部。即,将脚本放置于body标签的结束标签之前。这样就可以达到浏览器在加载和渲染页面内容之后,在下载这些脚本的同时用户可以感受到初始的内容。例如:

<html>
  <head>
  </head>
  <body>
    [Body content goes here.]
  <script src="mainscript.js"></script>
  </body>
</html>

An exception to this technique is any script that manipulates the initial content or DOM, or provides required page functionality prior to or during rendering. Critical scripts such as these can be put into a separate file and loaded in the page head as usual, and the rest can still be placed last thing in the page for loading only after the page is rendered.

但有一种特殊情况,一些脚本在页面渲染之前或者渲染过程中,就需要对初始的内容(initial content)、DOM进行操作或者需要保障一些必要的页面功能。这种情况,我们可以将这些关键性脚本(Critical script)放在一个单独的文件中,然后像通常的做法一样,将其放在页面的头部进行加载,同时,其他可以在页面渲染完成后才加载的脚本则放置在页面的最后。

The order in which resources must be loaded for maximum efficiency is called the Critical Rendering Path; you can find a thorough article about it at Bits of Code.

我们称这种使得哪些必须被加载的资源达到最高效加载效果的加载顺序为“关键渲染路径”(Critical Rendering Path);关于这个话题,在Bits of Code上面有一篇详尽的介绍。

代码位置(Code Location)

Of course, the technique described above splits your JavaScript into two files on the server and thus requires two HTTP requests instead of one, exactly the situation we're trying to avoid. A better solution for relocating critical, pre-render scripts might be to place them directly inside the page itself, referred to as an "inline push".

上面提到的这种技术,会将 JavaScript 分割成两个文件,因此也需要两次 HTTP 请求而不是一次,这种情形显然是需要尽量避免的。一个好的解决方法是,将那些关键性的、预加载的脚本直接提取到页面上,这称之为“内联推入(inline-push)”

Here, instead of putting the critical script in a separate file and referencing it in the page head, add a block, either in the head or in the body, and insert the script itself (not a file reference, but the actual script code) at the point at which it's needed. Assuming the script isn't too big, this method gets the script loaded along with the HTML and executed immediately, and avoids the extra HTTP request overhead of putting it in the page head.

就是说,不是将“关键性脚本”放在一个单独的文件中,并在页面的头部引用它;而是说,将脚本本身(不是文件引用,而是实实在在的代码)放在一个<script>...</script>块中,并将其插入需要的地方(要么是头部,要么是体部)。假设脚本不是很大,这种方法可以使得脚本伴随着HTML进行加载并即刻执行,进而避免额外的 HTTP 请求消耗。

For example, if a returning user's name is already available, you might want to display it in the page as soon as possible by calling a JavaScript function, rather than wait until after all the content is loaded.

举个例子,假设某个返回的用户名字已经可以获取到,那么你可能希望它可在页面中尽快展示,而不是等待所有的内容都已经加载完了才显示。

<p>Welcome back, <script>insertText(username)</script>!</p>

Or you might need an entire function to execute in place as the page loads, in order to render certain content correctly.

或者,有时你可能需要一个完整的函数随着页面的加载适当地执行,以此来正确地渲染内容。

<h1>Our Site</h1>

<h2 id="greethead">, and welcome to Our Site!</h2>

<script>
//insert time of day greeting from computer time
var hr = new Date().getHours();
var greeting = "Good morning";
if (hr > 11) {
    greeting = "Good afternoon";
}
if (hr > 17) {
    greeting = "Good evening";
}
h2 = document.getElementById("greethead");
h2.innerHTML = greeting + h2.innerHTML;
</script>

<p>Blah blah blah</p>

This simple technique avoids a separate HTTP request to retrieve a small amount of code and allows the script to run immediately at its appropriate place in the page, at the minor cost of a few dozen extra bytes in the HTML page.

这种简单的技术,只花费了几十个字节的微小代价,却换来了,既避免了额外的HTTP请求,又使得脚本得以在它正确得位置立刻执行的效果。

总结 (Summary)

In this section, we covered ways to reduce the number of HTTP requests our pages make, and considered techniques for both text and graphical resources. Every round-trip to the server we can avoid saves time, speeds up the page load, and gets its content to our users sooner.

在本篇中,我们提到了一些减少HTTP请求次数的方法,考察了这方面一些针对文件和图像资源的技术。每一个我们可以避免的到服务器的来回,都可以加速页面的加载,进而可以将内容很快地呈现给用户。