《Web性能实战》读书笔记

2024-07-11 19:06:21 浏览数 (2)

Web性能调优一直是高级前端必须掌握的技能,市面上不少书简绍性能调优的书总是告诉读者一些理论性的东西,而如何去实践说的却不多,这本书不仅告诉读者Web性能优化的理论知识,同时还会告诉读者怎么用node去设置,是一本前端进阶必看的书。

《Web性能实战》《Web性能实战》

理解Web性能

Web性能主要指网站的加载速度。你可以通过提高网站速度来加快内容的传输,从而改善用户的体验。电子商务网站上近一半的用户希望在2秒内完成。如果加载时间超过3秒,40%的用户将会退出。页面相应时间每延迟1秒就意味着7%的用户不再进一步操作。

加载时间是用户请求网站到网站出现在用户屏幕上所经历的时间。

本节从减少传输的数据量入手,简单的简绍了3中提高性能的方法:缩小资源、使用服务器压缩、压缩图像。

缩小(minification)文本资源是从基于文本的资源中去除所有空白和非必要字符的过程,因而不会影响资源的工作方式。

缩小资源

下面命令-o表示输入的文件路径,通过使用下面命令缩小资源后 CSS文件缩小了14%,JS文件缩小了66%,HTML缩小了19%,缩小的还是挺可观的。

代码语言:javascript复制
# 缩小CSS
minify -o styles.min.css styles.css
# 缩小JS
minify -o jquery.min.js jquery.js
# 缩小HTML
htmlminify -o index.html index.src.html

使用服务器压缩

服务器压缩的工作方式是用户从服务器请求网页,用户的请求会附带一个Accept-Encoding的头信息,向服务器告知浏览器可以使用的压缩格式。如果服务器按照Accept-Encoding头信息中的内容进行编码,它将用一个Content-Encoding响应头信息进行回复,其值是所使用的压缩方式。gzip是比较常用的一种压缩格式,express服务配置gzip如下。

代码语言:javascript复制
var express = require("express");
// 需要运行 npm install compression 安装compression
var compression = require("compression");
var app = express();

// 使用compression中间件
app.use(compression());
app.use(express.static(__dirname));
app.listen(8080);

通过上述两行代码,压缩后资源缩小了66%。

压缩图像

压缩图像书中简绍了使用常用的TinyPNG去压缩,大小缩小了60%左右。

通过这三种方式,网站的加载速度提高了近70%,还是非常可观的。

使用评估工具

第一个评估工具就是Google PageSpeed Insights,去该网站输入你要分析的网站,它会给你一些优化的建议,当然网页得是已经在线上跑的网页,另外国内需要翻墙。

第二个评估工具是Google Analytics,这个工具比较全面,但是需要在网页中注入JS脚本,如果是大公司的开发者,注入Google的代码往往需要走法律审查,因为安装跟踪代码时,需要接受法律协议条款。

首字节时间(Time to First Byte,TTFB):从用户请求网页到相应第一个字节到达之间的时间。首字节时间往往跟队列请求、DNS查找、连接设置和SSL握手等有关。

页面创建过程:解析HTML以创建DOM -> 解析CSS以创建CSSOM -> 布局元素 -> 绘制页面。

优化CSS

移动优先

移动优先响应式设计:默认样式为移动设备定义,并且随着屏幕宽度的增大而增加复杂性。 桌面优先响应式设计:默认样式为桌面设备定义,并且随着屏幕宽度的减小而降低复杂性。

通常响应式设计中使用移动优先的响应式设计会更好一点,主要的原因有: 1.通常移动设备的处理能力和内存通常低于桌面设备,使用移动优先不需要解析媒体查询。 2.从开发角度出发,扩大样式规模更容易实现。 3.手机用户量激增,搜索引擎对移动设备逐渐更友好。

避免使用@import声明

CSS中可以使用@import来引入一个样式,使用方式如下:

代码语言:javascript复制
@import url(fonts.css);

最好不要使用@import来引入一个样式,因为@import是串行的,会增加页面的总体加载和渲染时间。可以使用<link>标签来代替,因为<link>标签是并行的。 Less中的@import最终编译到css中的并不是CSS语法中的@import,所以可以使用。

在<head>中放置CSS

<head>标签中放置CSS要比在<body>标签中放置CSS有两个好处:

  1. 无样式内容闪烁的问题;
  2. 加载时提高页面的渲染性能。

如果CSS放在<body>标签中,如果放在页面HTML结构的下方那么就会先渲染一个没有自定义样式的页面,等加载完CSS以后才会有自定义样式,所以会有无样式内容闪烁的问题。 放在<body>中还有一个问题是页面加载完<body>中的样式以后会重新渲染和绘制整个DOM,页面渲染性能较差。

使用CSS过渡

CSS过渡的优点:

  1. 广泛支持;
  2. 回流复杂DOM时,CPU的使用效率更高;
  3. 无额外开销。

如果动画可以使用CSS过渡来实现的话,最好使用CSS过渡而不是JS来改变DOM(减少回流)。

使用will-change来优化过渡

will-change使用方法:

代码语言:javascript复制
will-change: 属性1, [属性2]...

/* 如: */
will-change: background-color;

will-change可以告诉浏览器哪个属性将会过渡,但是不要使用will-change: all;

其他的优化点:

  1. 使用简写属性;
  2. 使用CSS潜选择器;
  3. 分割CSS不加载当前页面中不会显示的CSS;
  4. 尽可能使用flexbox布局。

关键CSS技术

关键CSS,即折叠之上的内容,这些是用户会立即看到的内容,需要尽快加载。 非关键CSS,即折叠之下的内容,这些是用户开始向下滚动页面之前看不到的内容样式,这种CSS也应该尽快加载,但不能在关键CSS之前加载。

书中的折叠是指屏幕的底部,实际上关键CSS就是首屏样式,非关键CSS就是非首屏的样式。

渲染阻塞指的是阻止浏览器将内容绘制到屏幕的任务活动,这是Web中不可避免的事情。无论使用<link>还是@import引入样式都会产生渲染阻塞(虽然<link>下载是并行的)。

加载首屏样式:为了减少渲染阻塞时间可以直接把关键CSS样式放在<style>标签中。

加载非首屏样式:非首屏样式也会遇到渲染阻塞的问题,可以使用preload来减少阻塞渲染时间。

代码语言:javascript复制
<link rel="preload" href="index.css" as="style" onload="this.rel='stylesheet'">
  <noscript>
    <link rel="stylesheet" href="index.css" />
  </noscript>
</link>

响应式图像

通过媒体查询来适配高DPI显示器

代码语言:javascript复制
/* 正常屏幕 */
#masthead {
  background-image: url("../img/masthead-small.jpg");
}

/* 高分别率 */
@media screen (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) and (min-width: 44em){ /* High DPI 704px/16px */
  #masthead {
    background-image: url("../img/masthead-small.jpg");
  }
}

@media screen (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) and (min-width: 56em){ /* High DPI 896px/16px */
  #masthead {
    background-image: url("../img/masthead-medium.jpg");
  }
}

@media screen (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) and (min-width: 77em){ /* High DPI 1232px/16px */
  #masthead {
    background-image: url("../img/masthead-large.jpg");
  }
}

-webkit-min-device-pixel-ratio是旧的浏览器对高DPI的支持,如果比率是1相当于是96DPI,2相当于是192DPI;min-resolution是现代浏览器所支持的直接显示值就行了,最后min-width根据屏幕的宽度来加载不同大小的图片。

CSS中使用SVG

CSS可以直接把SVG当做图片来使用,实际上本身也可以看成图片:

代码语言:javascript复制
background-image: url(../img/masthead.svg);

HTML中传输图片

图片全局max-width规则:在响应式网站中图片往往最大是屏幕的宽度,所以显示最大宽度100%会很有用的。

代码语言:javascript复制
img {
  max-width: 100%;
}

商品可以通过媒体查询根据设备分别率来使用不同的图片,实际上H5中,img标签的srcset和sizes属性也可以实现类似的功能。srcset可以根据屏幕的宽度来加载不同的图片。sizes可以通过屏幕的宽度设置图片的宽度,如下。

代码语言:javascript复制
<img
  src="img/amp-xsmall.jpg"
  class="articleImageFull"
  srcset="img/amp-small.jpg 512w,
         img/amp-medium.jpg 768w,
         img/amp-large.jpg 1280w"
  sizes="(min-width: 704px) 50vw, (min-width: 480px) 75vw, 100vw"
/>

上述代码中在512px像素宽度的时候图片是img/amp-small.jpg,768px像素宽度的时候图片是img/amp-medium.jpg,1280px像素宽度的时候图片是img/amp-large.jpg,可见后面的w指的是屏幕宽度为多少。同样的在最小宽度704px的时候图片的宽度是宽度的50%,最小宽度是480px的时候图片的宽度是75%,最小宽度更小的时候图片的宽度是100%。

如果需要在相同的宽度的时候,根据设备分别率来显示不同的图片,那么srcset和sizes就不能做了,此时可以考虑功能更强大的picture标签,如下。

代码语言:javascript复制
<picture>
  <source
    media="(min-width: 704px)"
    srcset="img/apm-medium.jpg 348w,img/amp-large.jpg 512w"
    sizes="33.3vw"
  >
  <source
    srcset="img/apm-cropped-small.jpg 1x,img/amp-cropped-medium.jpg 2x"
    sizes="75vw"
  >
  <img src="img/amp-small.jpg">
</picture>

可以使用type属性来加载webp图片,如下。如果支持的话使用图片img/apm-small.webp,不支持的话使用图片img/amp-small.jpg

代码语言:javascript复制
<picture>
  <source srcset="img/apm-small.webp" type="webp">
  <img src="img/amp-small.jpg">
</picture>

picture很好用,如果不支持的时候可以走img标签做兜底处理,如果在低版本浏览器也希望使用picture标签该怎么办,就得靠picturefill了,使用方式如下。下面第一个script用来创建一个picture元素,防止因为没有该元素而导致解析错误,第二个script用来异步下载库文件。

代码语言:javascript复制
<script>document.createElement("picture");</script>
<script src="js/picturefill.min.js" async></script>

图像的进一步处理

使用雪碧图

雪碧图的好处:将大量图片缩减为单个图片,可以更加高效地传递资源,并通过减少到Web服务器的连接数来缩短页面的加载时间。

注:使用雪碧图可以减少HTTP请求,但在HTTP2中是反模式。

生成雪碧图:

代码语言:javascript复制
npm install -g svg-sprite
svg-sprite -css -css-render-less -css-dest=less -css-sprite=../img/icons.svg --css-layout=diagonal img/icon-images/*.svg

将雪碧图回退为图片可以使用这个工具:https://github.com/filamentgroup/grumpicon

缩小图像

书中减少使用imagemin来缩小图片jpeg和png的图片,同时也支持生成webp图片: https://github.com/imagemin/imagemin

imagemin提供了大量的插件:https://www.npmjs.com/search?q=keywords:imageminplugin

使用svgo来压缩svg图片:https://github.com/svg/svgo

使用懒加载

懒加载简单实现:

代码语言:javascript复制
(function(window, document){
  "use strict";
  // 懒加载对象
  var lazyLoader = {
    lazyClass: "lazy",
    images: null,
    processing: false,
    throttle: 200,
    buffer: 50,
    // 初始化
    init: function(){
      lazyLoader.images = [].slice.call(document.getElementsByClassName(lazyLoader.lazyClass));
      lazyLoader.scanImages();
      document.addEventListener("scroll", lazyLoader.scanImages);
      document.addEventListener("touchmove", lazyLoader.scanImages);
    },
    // 销毁
    destroy: function(){
      document.removeEventListener("scroll", lazyLoader.scanImages);
      document.removeEventListener("touchmove", lazyLoader.scanImages);
    },
    // 扫描图片
    scanImages: function(){
      if(document.getElementsByClassName(lazyLoader.lazyClass).length === 0){
        lazyLoader.destroy();
        return;
      }

      if(lazyLoader.processing === false){
        lazyLoader.processing = true;

        setTimeout(function(){
          for(var i in lazyLoader.images){
            if(lazyLoader.images[i].className.indexOf("lazy") !== -1){
              if(lazyLoader.inViewport(lazyLoader.images[i])){
                lazyLoader.loadImage(lazyLoader.images[i]);
              }
            }
          }

          lazyLoader.processing = false;
        }, lazyLoader.throttle);
      }
    },
    // 判断图片是否出现在屏幕上
    inViewport: function(img){
      var top = ((document.body.scrollTop || document.documentElement.scrollTop)   window.innerHeight)   lazyLoader.buffer;
      return img.offsetTop <= top;
    },
    // 加载图片
    loadImage: function(img){
      if(img.parentNode.tagName === "PICTURE"){
        var sourceEl = img.parentNode.getElementsByTagName("source");

        for(var i = 0; i < sourceEl.length; i  ){
          var sourceSrcset = sourceEl[i].getAttribute("data-srcset");

          if(sourceSrcset !== null){
            sourceEl[i].setAttribute("srcset", sourceSrcset);
            sourceEl[i].removeAttribute("data-srcset");
          }
        }
      }

      var imgSrc = img.getAttribute("data-src"),
        imgSrcset = img.getAttribute("data-srcset");

      if(imgSrc !== null){
        img.setAttribute("src", imgSrc);
        img.removeAttribute("data-src");
      }

      if(imgSrcset !== null){
        img.setAttribute("srcset", imgSrcset);
        img.removeAttribute("data-srcset");
      }

      lazyLoader.removeClass(img, lazyLoader.lazyClass);
    },
    // 移除样式
    removeClass: function(img, className){
      var classArr = img.className.split(" ");

      for(var i = 0; i < classArr.length; i  ){
        if(classArr[i] === className){
          classArr.splice(i, 1);
        }
      }

      img.className = classArr.toString().replace(",", " ");
    }
  };

  // 启动初始化程序
  document.onreadystatechange = lazyLoader.init;
})(window, document);

上述懒加载的使用:图片上添加lazy样式,同时使用data-src代替src,src使用默认未加载图片来代替,如:

代码语言:javascript复制
<img src="img/blank.png" data-src="img/red-snapper-1x.jpg" class="recipeImage lazy">

更快的字体

将ttf字体转换为其他字体:

代码语言:javascript复制
npm install -g ttf2eot ttf2woff ttf2woff2
ttf2eot OpenSans-Light.ttf OpenSans-Light.eot
ttf2woff OpenSans-Light.ttf OpenSans-Light.woff
cat OpenSans-Light.ttf | ttf2woff2 >> OpenSans-Light.woff2

CSS中使用自定义字体:

代码语言:javascript复制
/* 定义字体 */
@font-face{
  font-family: "Open Sans Light";
  font-weight: 300;
  font-style: normal;
  src:
    local("Open Sans Extra Light"),
    local("OpenSans-Light"),
    url("open-sans/OpenSans-Light.woff2") format("woff2"),
    url("open-sans/OpenSans-Light.woff") format("woff"),
    url("open-sans/OpenSans-Light.eot") format("embedded-opentype"),
    url("open-sans/OpenSans-Light.ttf") format("truetype");
}

.font-osl {
  /* 使用字体 */
  font-family: "Open Sans Light";
}

使用unicode-range加载字体子集,如下面把Open Sans Light拆分成BasicLatin部分和Cyrillic部分,使用unicode-range定义字符范围,如果文字中只有BasicLatin部分那么只会下载上面的文字,如果只有Cyrillic的文字那么下载下面的文字,都有会都下载。

代码语言:javascript复制
@font-face{
  font-family: "Open Sans Light";
  font-weight: 300;
  font-style: normal;
  src:
      url("open-sans/OpenSans-Light-BasicLatin.woff2") format("woff2"),
      url("open-sans/OpenSans-Light-BasicLatin.woff") format("woff"),
      url("open-sans/OpenSans-Light-BasicLatin.eot") format("embedded-opentype"),
      url("open-sans/OpenSans-Light-BasicLatin.ttf") format("truetype");
  unicode-range: U 0000-007F;
}

@font-face{
  font-family: "Open Sans Light";
  font-weight: 300;
  font-style: normal;
  src:
      url("open-sans/OpenSans-Light-Cyrillic.woff2") format("woff2"),
      url("open-sans/OpenSans-Light-Cyrillic.woff") format("woff"),
      url("open-sans/OpenSans-Light-Cyrillic.eot") format("embedded-opentype"),
      url("open-sans/OpenSans-Light-Cyrillic.ttf") format("truetype");
  unicode-range: U 0400-045F,U 0490-0491,U 04B0-04B1;
}

.font-osl {
  /* 使用字体 */
  font-family: "Open Sans Light";
}

字体下载和字体实际显示直接可能有一段时间,那么这段时间内的字体是怎么显示呢?可以使用font-display来控制:

font-display: auto; 默认值,类似于block。 font-display: block; 阻塞文本渲染,直到关联的字体加载完成。 font-display: swap; 显示回退文本,加载字体后显示自定义字体。 font-display: fallback; auto和swap的折中方案,短时间(100ms)内显示空白,之后显示回退文本,如果字体加载完后,显示自定义字体。 font-display: optional; 几乎和fallback一样,只是浏览器有更大的自由度来控制。

但是font-display并未得到广泛的支持,使用JS来做回退处理:

代码语言:javascript复制
(function(document){
  if(document.fonts && document.cookie.indexOf("fonts-loaded") === -1){
    document.fonts.load("1em Open Sans Light");
    document.fonts.load("1em Open Sans Regular");
    document.fonts.load("1em Open Sans Bold");

    document.fonts.ready.then(function(fontFaceSet){
      document.documentElement.className  = " fonts-loaded";
      document.cookie = "fonts-loaded=";
    });
  } else {
    // 添加一个fonts-loaded样式 需要字定义CSS有该样式的时候才使用字体
    document.documentElement.className  = " fonts-loaded";
  }

})(document);

保持JavaScript的简洁与快速

script标签会阻塞页面的渲染,放在body最后面有助于加快页面加载速度。

script带有async属性与不带async的区别: 不带async:下载脚本->脚本下载完成->浏览器等待其他脚本->执行脚本 带有async:下载脚本->脚本下载完成->执行脚本

带有async脚本下载完会立即执行而不会阻塞渲染。

使用async时需要注意,async下载完会立即执行那么,有可能执行的顺序跟script标签的顺序不同,从而导致JS执行报错。如有一个jquery.min.js文件,还有一个behaviors.js文件,其中behaviors.js引用到jquery.min.js中的(jQuery对象),那么两个都用async就可能就会在behaviors.js中的出现没有定义的情况。

解决方法: 1.可以把两个文件文件合并成一个

代码语言:javascript复制
# linux 合并两个文件:
cat jquery.min.js jquery.min.js > script.js
# windows 合并两个文件:
type jquery.min.js jquery.min.js > script.js
  1. 使用AMD:https://github.com/requirejs/alameda
代码语言:javascript复制
<script src="js/alameda.js" data-main="js/behaviors" async >
代码语言:javascript复制
// js/behaviors.js 中使用AMD模块
requirejs.config({
  paths: {
    jquery: 'juqery.min'
  }
});

require(['jquery'], function($) {
  // 其他代码

});
  1. 使用defer。

书中还简绍了jQuery的替代方案和用原生JS代替jQury,现在MVVM时代很少用jQuery了,就不简绍了,原生方案可以看这里:https://github.com/nefe/You-Dont-Need-jQuery。

使用Service Worker提升性能

Service Worker在单独的线程上工作,无法访问window对象,但可以通过中介(如postMessage API)间接访问。

使用方式:

  1. 注册Server Worker
代码语言:javascript复制
if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("/sw.js");
}
  1. 编写Server Worker代码
代码语言:javascript复制
// 文件/sw.js

// 案例1,添加资源的离线缓存
var cacheVersion = "v2",
    cachedAssets = [
        "/css/global.css?v=1",
        "/js/debounce.js",
        "/js/nav.js",
        "/js/attach-nav.js",
        "/img/global/jeremy.svg",
        "/img/global/icon-github.svg",
        "/img/global/icon-email.svg",
        "/img/global/icon-twitter.svg",
        "/img/global/icon-linked-in.svg",
    ];
// 添加缓存
self.addEventListener("install", function(event) {
  event.waitUntil(caches.open(cacheVersion).then(function(cache) { // chashes -> caches
      return cache.addAll(cachedAssets); // chache -> cache
  }).then(function() {
      return self.skipWaiting();
  }));
});

// skipWaiting 的时候会触发
self.addEventListener("activate", function(){
    return self.clients.claim();
});

// 案例2,拦截并缓存网络请求
self.addEventListener("fetch", function(event) {
  var allowedHosts = /(localhost|fonts.googleapis.com|fonts.gtatic.com)/i,
  deniedAssets = /(sw.js|sw-install.js)$/i,
  htmlDocument = /(/|.html)$/i;
  if(allowedHosts.test(event.request.url) === true && deniedAssets.test(event.request.url) === false) {
      if (htmlDocument.test(event.request.url) === true) {
          event.respondWith(
              fetch(event.request).then(function(response) {
                  caches.open(cacheVersion).then(function(cache) {
                      cache.put(event.request, response.clone());
                  });
                  return response;
              }).catch(function() {
                  return caches.match(event.request);
              })
          );
      } else {
          event.respondWith(
              caches.match(event.request).then(function(cachedResponse) {
                  return cachedResponse ||
                  fetch(event.request).then(function(fetchedResponse) {
                      caches.open(cacheVersion).then(function(cache) {
                          cache.put(event.request, fetchedResponse);
                      });
                      return fetchedResponse.clone();
                  });
              })
          );
      }
  }
});

// 案例3,清理缓存
self.addEventListener("activate", function(event) {
  var chacheWhitelist = ["v2"];
  event.waitUnitil(
      caches.keys().then(function(keyList) {
          return Promise.all([
              keyList.map(function(key){
                  if(chacheWhitelist.indexOf(key) === -1){
                      return caches.delete(key);
                  }
              }), selfclients.claim()
          ]);
      })
  );

});

微调资源传输

上面在使用gzip的时候使用了compression中间件,实际上compression是支持传入压缩等级的 范围是0~9,默认是6,但并不是越高越好,往往默认值的效果是比较好的。 另外compression也支持压缩特定的资源,可以使用filter,返回为true的时候表示压缩,false不压缩。

代码语言:javascript复制
app.use(compression({
  level: 7,
  filter: function (request, response) {
    // 一般是根据request来判断,这里直接返回true则都压缩
    return true;
  }
}));

使用Brotli压缩

Accept-Encoding中如果有br说明支持Brotli压缩,express可以使用shrink-ray来开启Brotli压缩。

代码语言:javascript复制
// 假设你已经运行过: npm install https shrink-ray
var express = require("express"),
  https = require("https"),
  shrinkRay = require("shrink-ray"),
  fs = require("fs"),
  path = require("path"),
  app = express(),
  pubDir = "./htdocs";

app.use(shrinkRay({
  // 缓存设置可以换成 false 表示不开启,书中为了测试性能把缓存关了
  cache: function(request, response){
    return false;
  },
  brotli: {
    quality: 11
  }
}));

app.use(express.static(path.join(__dirname, pubDir)));

https.createServer({
  key: fs.readFileSync("crt/localhost.key"),
  cert: fs.readFileSync("crt/localhost.crt")
}, app).listen(8443);

brotli中的quality范围为0~11,值越大文件越小,默认是4,一般也够用了。

设置缓存

设置Cache-Control头部的max-age指令

代码语言:javascript复制
app.use(express.static(path.join(__dirname, pubDir), {
  maxAge: '10s',
}));

响应头会带有:Cache-Control: max-age=10,注意max-age的单位是秒。

Cache-Control:no-cache: 向浏览器表明,下载的任何资源都可以储存在本地,但浏览器必须始终通过服务器重新验证资源。 Cache-Control:no-store: 比no-cache更近一步,它表示浏览器不应存储受影响的资源。要求浏览器每次访问页面时下载所有受影响的资源。 Cache-Control:stale-while-revalidate=10: 与max-age类似,单位也是秒,当资源过期后仍然使用过期的资源,同时发出请求并缓存新的资源,下次再请求的时候使用新的资源。

在CDN的Cache-Control有时会与privitepublic连用,如Cache-Control: privite, max-age=10,其中privite表示中介(CDN)不在其服务器上缓存资源,public则缓存。

对不同资源设置不同的缓存策略:

资源类型

修改频率

Cache-Control头部值

HTML

可能频繁修改,但需要尽可能保持最新

private, no-cache, max-age=3600

CSS和JS

可能每月修改

public, max-age=2592000

图片

几乎不会修改

public, max-age=31536000,

代码实现:

代码语言:javascript复制
app.use(express.static(path.join(__dirname, pubDir), {
  setHeaders: function(res, path){
    var fileType = mime.lookup(path);

    switch(fileType){
      case "text/html":
        res.setHeader("Cache-Control", "private, no-cache, max-age="   (60*60));
      break;

      case "text/javascript":
      case "application/javascript":
      case "text/css":
        res.setHeader("Cache-Control", "public, max-age="   (60*60*24*30));
      break;

      case "image/png":
      case "image/jpeg":
      case "image/svg xml":
        res.setHeader("Cache-Control", "public, max-age="   (60*60*24*365));
      break;
    }
  }
}));

资源提示

preconnect、prefetch与preload的使用:

代码语言:javascript复制
<link ref="preconnect" src="https://code.jquery.com">
<link ref="prefetch" src="https://code.jquery.com/jquery-2.2.4.min.js" as="script">
<link ref="preload" src="https://code.jquery.com/jquery-2.2.4.min.js" as="script">

preconnect可以提供更早的DNS查询,但是如果跟HTML是同域名的时候是没用什么用的,因为已经查询过DNS了,preconnect主要是为了查询其他域名,由于HTML是自上而下解析的,通常把preconnect放在HTML的head中的上面的位置。

prefetch告诉浏览器下载特定的资源,并将其存储到浏览器缓存中。通常用来预取同一个页面的资源,或者优先缓存下一页的资源。缓存下一页的资源使用时要小心,不要下载下页没有的资源,否则会造成过多的请求。

preload如果没有as属性,可能会导致请求2次的情况,另外preload只会缓存本页面的资源。

HTTP2未来展望

HTTP1的问题:

  1. 队头阻塞:HTTP1无法处理超过一小批的请求(通常认为是6个,因浏览器而异)。请求按接收顺序响应,在初始批处理中的所有请求完成之前,无法开始新的请求。如总共有9个任务,第一批会一次性加载6个,得等这6个中最慢的加载完后才会加载下一批的剩余3个请求。可以通过域名分片(不同域名加载不同批的资源)来处理,但实现起来比较繁琐。
  2. 未压缩头部:之前zip、br等压缩处理压缩的都是响应体,但是头部信息不能压缩,而有的时候头部信息甚至比响应体更大。
  3. 不安全网站:HTTP1可以不用实现SSL。

HTTP2对上述问题的处理

  1. 不再有队头阻塞:HTTP2通过实现新的通信体系结构来并行满足更多请求。新的信道使用一个连接并行处理多个请求,连接的构成: 是服务器和浏览器之间的双向通信通道,一个连接可以有多个流。 消息由流封装,单个消息相当于HTTP1的一次请求或一次响应。 由消息封装,帧是消息的分割符。如响应消息中的HEADERS帧表明下一数据表示响应的HTTP头,响应消息中的DATA帧表示下一数据是所请求的内容。
  2. 头部压缩:使用了HPACK压缩算法来解决这个问题,不仅压缩头部数据还通过创建一个表来存储重复的头部,以删除多余的头部。
  3. 强制HTTPS:HTTP2必须实现SSL,因此HTTP2一定是HTTPS。

HTTP2对不支持HTTP2的浏览器的处理:每个HTTP2服务器底层都应有一个HTTP1服务器在等待一个不支持HTTP2的客户端出现。

HTTP2的简单使用:

代码语言:javascript复制
var fs = require("fs"),
  path = require("path"),
  http2 = require("spdy"),
  mime = require("mime"),
  jsdom = require("jsdom"),
  pubDir = path.join(__dirname, "/htdocs");

var server = http2.createServer({
  key: fs.readFileSync(path.join(__dirname, "/crt/localhost.key")),
  cert: fs.readFileSync(path.join(__dirname, "/crt/localhost.crt"))
}, function(request, response){
  var filename = path.join(pubDir, request.url),
    contentType = mime.lookup(filename),
    protocolVersion = request.isSpdy ? "http2" : "http1";

  // 设置缓存响应头
  if((filename.indexOf(pubDir) === 0) && fs.existsSync(filename) && fs.statSync(filename).isFile()){
    response.writeHead(200, {
      "content-type": contentType,
      "cache-control": "max-age=3600"
    });

    // http1 的回退处理
    if(protocolVersion === "http1" && filename.indexOf(".html") !== -1){
      fs.readFile(filename, function(error, data){
        jsdom.env(data.toString(), function(error, window){
          window.document.documentElement.classList.add(protocolVersion);

          var scripts = window.document.querySelectorAll("script:not([crossorigin])"),
            jQueryScript = window.document.querySelector("script[crossorigin]"),
            concatenatedScript = window.document.createElement("script");
            concatenatedScript.src = "js/scripts.min.js";

          for(var i in scripts){
            scripts[i].remove();
          }

          jQueryScript.parentNode.insertBefore(concatenatedScript, jQueryScript.nextSibling);

          var newDocument = "<!doctype html>"   window.document.documentElement.outerHTML;
          response.end(newDocument);
        });
      });
    } else{
      // http2 流
      var fileStream = fs.createReadStream(filename);
      fileStream.pipe(response);
      fileStream.on("finish", response.end);
    }
  } else{
    response.writeHead(404);
    response.end();
  }
});

server.listen(8443);

0 人点赞