页面性能优化

# 页面性能优化

可参考:雅虎前端优化的35条军规 (opens new window)

降低请求量:合并资源,减少HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。

加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。

缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存localStorage。

渲染:JS/CSS优化,加载顺序,服务端渲染,pipeline。

  1. 网络层面
    • 请求过程的优化-HTTP请求优化(构建工具性能调优,Gzipd压缩原理,图片优化)
    • 减少网络请求-本地存储(浏览器的缓存机制,离线存储技术)
  2. 渲染层面
    • 服务端渲染的探索与实践
    • 浏览器的渲染机制解析(CSS性能方案,JS性能方案)
    • DOM优化(事件循环与异步更新,回流与重绘)
    • 首屏渲染提速(懒加载)
  3. 性能监测
    • 可视化工具(Performance,LightHouse)
    • W3C性能API

[TOC]

提升页面性能优化的方法有哪些:

  • 1、资源压缩合并,减少http请求
  • 2、非核心代码异步加载 --> 异步加载的方式 --> 异步加载的区别

如果回答出非核心代码异步加载,就会层层深入。

  • 3、利用浏览器缓存 --> 缓存的分类 --> 缓存的原理

缓存是所有性能优化的方式中最重要的一步。

  • 4、使用CDN

浏览器第一次打开页面时,缓存是起不了作用的。

  • 5、DNS预解析

# 一、资源压缩合并

HTTP请求优化方向:

  1. 减少请求次数。
  2. 减少单次请求所花费的时间。

即,资源的压缩和合并。

参考链接:https://juejin.im/post/5b022bdf518825426d2d69fe (opens new window)

可以加快首次访问速度,减少http请求。

保护代码,降低代码的可读性。

# 1.1 html压缩

  • 压缩这些在文本文件中有意义,但是在HTML中不显示的字符

    • 空格,制表符,换行符注释
  • 压缩方式:

    • nodejs提供了html-minifier工具

    • 后端模板引擎渲染压缩

# 1.2 CSS压缩

  1. 无效代码删除
  2. css语义合并。
  • 压缩方式
    • 使用html-minifier工具
    • 使用clean-css对css压缩

# 1.3 JS压缩和混乱

js的压缩和混乱主要包括以下这几部分:

  1. 无效字符的删除
  2. 剔除注释
  3. 代码语义的缩减和优化
  4. 代码保护(代码逻辑变得混乱,降低代码的可读性,这点很重要)
  • 压缩方式
    • 使用html-minifier工具
    • 使用uglifyjs2对js进行压缩

# 1.4 合并文件

  • 合并图片(css sprites)、CSS和JS文件合并、CSS和JS文件压缩。
  • 压缩合并css和js可以减少网站http请求的次数,但合并文件可能会带来问题:首屏渲染和缓存失效问题。那该如何处理这问题呢?----公共库合并和不同页面的合并。
  • 合并方式
    • 使用nodejs实现文件合并(gulp、fis3)

# 1.5 webpack 性能调优与 Gzip 原理

# 1.5.1 构建过程提速

# 1.5.1.1 不要让loader做太多事情
  • include, exclude, 为loader添加相应的参数
module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader?cacheDirectory=true',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.5.1.2 不要放过第三方库
  • 使用 DllPlugin 代替 CommonsChunkPlugin,这个依赖库不会跟着业务代码一起被在重新打包,只有当依赖自身发生版本变化时才会重新打包。
# 1.5.1.3 Happypack
  • Happypack会充分利用CPU多核并发的优势,将任务分解给多个子进程进行并发执行,提高打包效率。
  • 相反,webpack是单线程的。

# 1.5.2 构建结果体积压缩

webpack4 现在已经默认使用 uglifyjs-webpack-plugin 对代码做压缩了——在 webpack4 中,我们是通过配置 optimization.minimize 与 optimization.minimizer 来自定义压缩相关的操作的。

  • tree-shaking 适用于处理模块级别的冗余代码。
    • 基于 import/export 语法,Tree-Shaking 可以在编译的过程中获悉哪些模块并没有真正被使用,这些没用的代码,在最后打包的时候会被去除。
# 1.5.2.1 按需加载
output: {
    path: path.join(__dirname, '/../dist'),
    filename: 'app.js',
    publicPath: defaultSettings.publicPath,
    // 指定 chunkFilename
    chunkFilename: '[name].[chunkhash:5].chunk.js',
},
1
2
3
4
5
6
7
const getComponent => (location, cb) {
  require.ensure([], (require) => {
    cb(null, require('../pages/BugComponent').default)
  }, 'bug')
},

<Route path="/bug" getComponent={getComponent}>
1
2
3
4
5
6
7

# 1.5.3 Gzip - HTTP压缩

HTTP压缩就是以缩小体积为目的,对HTTP内容进行重新编码的过程。

  • 只要不是1k、2k的小文件,压缩和解压的时间开销,相比传输过程节省下来的时间开销,是微不足道的。
  • Gzip压缩后通常能减少响应70%左右的大小。
  • 压缩原理:在文本文件中找出重复出现的字符串,并临时替换它们,从而使整个文件变小。
  • Gzip压缩是服务器的活,如果服务器了解到有Gzip的需求,则会启动CPU去完成任务,故若存在大量压缩需求,服务器也是扛不住的。因此,Webpack中的Gzip压缩就是为了在构建过程中做一部分服务器的工作,为服务器分压。

# 二、非核心代码异步加载

加快首屏响应速度。

# 2.1 异步加载的方式

# 2.1.1 动态脚本加载

在还没定义defer和async前,异步加载的方式是动态创建script。

function addScriptTag(src){  
    var script = document.createElement('script');  
    script.setAttribute("type","text/javascript");  
    script.src = src;  
    document.body.appendChild(script);  
}  
window.onload = function(){  
    addScriptTag("js/index.js");  
}
1
2
3
4
5
6
7
8
9

# 2.1.2 defer

  1. 立即下载,延迟执行(HTML解析完之后才会执行)。
  2. 按照加载的顺序依次执行js文件。
  3. 兼容所有浏览器。
<script src="./defer.js" defer></script>
1

# 2.1.3 async

  1. 立即下载,立即执行。

  2. 执行顺序和加载顺序无关。(谁先加载完就执行谁)

  3. HTmL5新增特性,在IE<=9时不支持。

  4. 仅适用于外部脚本。

<script src="./async.js" async></script>
1

# 2.2 异步加载的区别

异步加载

其中蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

  • 最稳妥的办法还是把<script>写在<body>底部,没有兼容性问题,没有白屏问题,没有执行顺序问题。
  • 当JS与DOM元素和其他脚本依赖关系不强时,可以选用async;当JS依赖于DOM元素和其他脚本的执行结果时,可以选用defer。

# 三、浏览器缓存

提升再次次访问速度,减少服务器压力。

缓存:资源文件(比如图片)在本地的硬盘里存有副本,浏览器下次请求的时候,可能直接从本地磁盘里读取,而不会重新请求图片的url。

# 缓存的机制

强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,则重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存

缓存机制

# 3.1 强缓存

强缓存:不用请求服务器,直接使用本地的缓存。

在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache或from memory cache。

**强缓存是利用 http 响应头中的ExpiresCache-Control实现的。**若命中强缓存,则浏览器直接从缓存中获取资源,不会与服务器发生通信。

浏览器第一次请求一个资源时,服务器在返回该资源的同时,会把上面这两个属性放在response header中。

Response Headers
	Expires Expires:Thu, 21 Jan 2017 23:2=39:02 GMT
	Cache-Control:max-age=3600
1
2
3

注意:这两个response header属性可以只启用一个,也可以同时启用。当response header中,Expires和Cache-Control同时存在时,Cache-Control的优先级高于Expires

# 3.1.1 Expires

服务器返回的绝对时间

http1.0的产物。浏览器再次请求这个资源时,先从缓存中寻找,找到这个资源后,拿出它的Expires跟当前的请求时间比较,如果请求时间在Expires的时间之前,就能命中缓存,否则就不行。

如果缓存没有命中,浏览器直接从服务器请求资源时,Expires Header在重新请求的时候会被更新。

缺点:

由于Expires是服务器返回的一个绝对时间,存在的问题是:服务器的时间和客户端的时间可能不一致。在服务器时间与客户端时间相差较大时,缓存管理容易出现问题。

所以,在http1.1中,提出了一个新的response header,就是Cache-Control。

# 3.1.2 Cache-Control

服务器返回的相对时间。(单位秒)

http1.1中新增的 response header。浏览器第一次请求资源之后,在接下来的相对时间之内,都可以利用本地缓存。超出相对事件则重新请求时,Cache-Control会被更新。

# 3.1.3 应用

对于有大量静态资源的网页,一定要利用强缓存,提高响应速度。通常的做法是,为这些静态资源全部配置一个超时时间超长的Expires或Cache-Control,比如京东首页缓存的资源,它的缓存过期时间都设置到了2026年。

cache-control: max-age=3600, s-maxage=31536000
1
  • s-maxage的优先级比max-age高,如果s-maxage未过期,则向代理服务器请求其缓存内容。
  • s-maxmessage仅在代理服务器中生效,客户端只考虑max-age。

# 3.1.4 其他

# 3.1.4.1 public 与 private
  • public
    • 可被浏览器和代理服务器缓存
  • private
    • 默认值
    • 仅被浏览器缓存

# 3.2 协商缓存

协商缓存

  1. 当浏览器请求某资源时,发现超出了强缓存设置的时间,就会发出请求到服务器,询问是否从缓存中读取资源。
  2. 服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源。
  • 协商缓存需要与cache-control共同使用。

# 3.2.1 Last-Modified、If-Modified-Since

过程如下:

(1)浏览器首次请求资源时,服务器在返回这个资源的同时,会加上Last-Modified这个 response header,表示这该资源在服务器上的最后修改时间。

(2)浏览器再次请求这个资源时,会加上If-Modified-Since这个 request header,即上一次返回的Last-Modified的值。

(3)服务器收到第二次请求时,会比对浏览器传过来的If-Modified-Since和资源在服务器上的最后修改时间Last-Modified,判断资源是否有变化。如果没有变化则返回304 Not Modified,但不返回资源内容(此时,服务器不会返回 Last-Modified );如果有变化,就正常返回资源内容(继续重复整个流程)。

(4)浏览器如果收到304的响应,就会从缓存中加载资源。

优点:

这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小得多,这样就节省了网络带宽。

缺点:

Last-ModifiedIf-Modified-Since一般来说都是非常可靠的,但面临的问题是:

  • 服务器上的资源变化了,但是最后的修改时间却没有变化
    • 如果服务器端在一秒内修改文件两次,但产生的Last-Modified却只有一个值。
  • 文件修改时间改了,但文件内容却没有变

这一对header就无法解决这种情况。

# 3.2.2 ETag、If-None-Match

根据文件内容是否修改来决定缓存策略。

ETag(Entity Tag):被请求变量的实体值。

过程如下:

(1)浏览器第一次请求一个资源,服务器在返回这个资源的同时,会加上ETag这个 response header,是服务器根据当前请求的资源生成的唯一标识。这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间无关。

(2)浏览器再次请求这个资源时,会加上If-None-Match这个 request header,即上一次返回的ETag的值。

3)服务器第二次请求时,会对比浏览器传过来的If-None-Match和服务器重新生成的一个新的ETag,判断资源是否有变化。如果没有变化则返回304 Not Modified,但不返回资源内容(此时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag并无变化)。如果有变化,就正常返回资源内容(继续重复整个流程)。

(4)浏览器如果收到304的响应,就会从缓存中加载资源。

  • 两者之间对比

    • 精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
    • 性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
    • 优先级上,服务器校验优先考虑Etag
  • 浏览器缓存知识小结及应用 (opens new window)[荐]

# 3.2 用户行为对浏览器缓存的影响

1.地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;

2.F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断;

3.ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。

# 四、CDN

提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。

对于首次访问的加速,我们需要从网络层面进行优化,最常见的手段就是CDN(Content Delivery Network,内容分发网络)加速。通过将静态资源(例如javascript,css,图片等等)缓存到离用户很近的相同网络运营商的CDN节点上。

CDN

当请求达到CDN节点后,节点会判断自己的内容缓存是否有效。如果有效,则立即响应缓存内容给用户,从而加快响应速度。如果CDN节点的缓存失效,它会根据服务配置去我们的内容源服务器获取最新的资源响应给用户,并将内容缓存下来以便响应给后续访问的用户。

一个地区内只要有一个用户先加载资源,在CDN中建立了缓存,该地区的其他后续用户都能因此而受益

# 五、DNS

# 5.1 DNS预解析(dns-prefetch)

通过 DNS 预解析来告诉浏览器未来我们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成 DNS 解析。

DNS 请求需要的带宽非常小,但是延迟却有点高,这点在手机网络上特别明显。DNS预解析 能让延迟明显减少一些,例如用户点击链接时。在某些情况下,延迟能减少一秒钟。

最明显的例子,DNS预解析在某个页面中包含非常多的域名非常有效,如搜索结果页。

# 5.1.1 第一步:打开或关闭DNS预解析

  1. 方式一:服务器端发送 X-DNS-Prefetch-Control 报头。
  2. 方式二:
<!-- http-equiv 属性可用于模拟一个 HTTP 响应头 -->
<!-- 强制打开浏览器的预解析 -->
<meta http-equiv="x-dns-prefetch-control" content="on">
1
2
3

在一些高级浏览器中,页面中所有的超链接(<a>标签),默认打开了DNS预解析。

但是,如果页面中采用的https协议,很多浏览器是默认关闭了超链接的DNS预解析,防止窃听者根据DNS Prefetching推断显示在HTTPS页面中超链接的主机名。

# 5.1.2 第二步:对指定的域名进行DNS预解析

如果我们将来可能从 smyhvae.com 获取图片或音频资源,那么可以在文档顶部的 标签中加入以下内容:

<link rel="dns-prefetch" href="http://www.smyhvae.com/">
1

当我们从该 URL 请求一个资源时,就不再需要等待 DNS 解析的过程。该技术对使用第三方资源特别有用。

参考链接:

  • 我们在请求一些静态资源文件的时候,经常都会将这些静态资源放到二级域名下去请求访问。比如www.4399.com (opens new window)请求static.4399.com/j/main.js,是为了

    • cookie隔离,减少主域进行http请求时携带的cookie。

    • 方便静态资源做CDN加速。

  • 放置于独立域名下更加安全,防止非法人员进行随意引用。

# 5.2 DNS缓存

参考教程: 一文搞懂DNS缓存 (opens new window)