性能优化的方方面面

1,438 阅读15分钟

总览:

性能调优工具

chrome devtool: Network

能力: 查看网络请求以及资源加载的耗时

  • Queueing 浏览器将资源放入队列时间
  • Stalled 因放入队列时间而发生的停滞时间
  • DNS Lookup DNS解析时间
  • Initial connection 建立HTTP连接的时间
  • SSL 浏览器与服务器建立安全性连接的时间
  • TTFB 等待服务端返回数据的时间
  • Content Download 浏览器下载资源的时间

chrome devtool: Lighthouse

  • First Contentful Paint 首屏渲染时间,1s以内绿色
  • Speed Index 速度指数,4s以内绿色
  • Time to Interactive 到页面可交换的时间

chrome devtool: Performance

专业的网站性能分析工具

webPageTest

可以模拟不同场景下访问的情况, 比如不同的浏览器, 不同的国家等等.

webPageTest

webpack-bundle-analyzer

资源打包分析工具

前端性能参数

可以通过如下方法获取前端页面加载的时间:

window.addEventListener('DOMContentLoaded', (event) => {
    let timing = performance.getEntriesByType('navigation')[0];
    console.log(timing.domInteractive);
    console.log(timing.fetchStart);
    let diff = timing.domInteractive - timing.fetchStart;
    console.log("TTI: " + diff);
})

更多的性能参数如下:

DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数: redirectCount
重定向耗时: redirectEnd - redirectStart

资源优化

图片资源优化

  • 图片资源转CDN

    把图片资源转存CDN有几个好处:

    第一, 我们知道浏览器中的同域网页并发数量是受到控制的, 其并发限制如下:

    把图片资源转存CND可以让图片请求不与主域抢网络资源

    第二, 目前很多CDN可以动态裁剪和压缩图片, 这样在网站中就可以按照实际的显示尺寸加载对应分辨率的图片, 防止前端对图片进行缩放.

    不要对图片进行了缩放的理由有两个:

    1. html缩小图片只缩小了尺寸, 图片会失真
    2. 缩放意味着图片大小不合适, 网页加载开销会偏高
  • 雪碧图(CSS sprite)

    雪碧图的目的也是减少http网络请求的数量, 原理不再赘述, 此外雪碧图一定程度上可以减小图片的总大小

  • 图片懒加载

    用js判断当该图片在页面中可见的时候再设置src值

  • 响应式图片

    除了控制cdn参数动态裁剪图片意外, 还可以用原生的响应式图片来在不同的环境下切换不同的图片资源:

    <picture>
        <source srcset="banner_w1000.jpg" media="(min-width: 801px)">
        <source srcset="banner_w800.jpg" media="(max-width: 800px)">
        <img src="banner_w800.jpg" alt="">
    </picture>
    
  • 压缩图片的大小

    压缩图片的大小, 不需要透明背景就把png图片转化为jpg, 并且通过无损压缩可以减少图片的体积

    在允许的情况下, 使用webp格式的图片能进一步压缩图片的大小

  • 对于简单效果, 用css替代图片

资源预加载

preload

在标签上添加preload, 这些资源会在页面加载的生命周期的早起阶段就开始获取. 而不是等到具体渲染的时候

详细的草案可以看这里

<link rel="preload" href="style.css" as="style as="..." onload="preloadFinished()">
<link rel="preload" href="main.js" as="script as="..." onload="preloadFinished()">

preload的特点如下:

  • 将加载和执行分离, 提前加载, 在需要的时候执行
  • 不论资源是否需要, 都会进行加载
  • perload有as属性, 可以设置正确的资源加载优先级, 比如as="style"会获得最高的优先级, 设置as="script"会获得低或者中优先级
  • 可以定义资源的onload事件
  • 对于跨域资源进行preload的时候, 必须添加crossorigin属性
  • preload字体如果没有crossorigin会进行二次获取, 字体文件会被下载两次
  • 没有用到的preload资源在chrome的console中会在onload后的3s后发生警告
  1. preload和HTTP2主动推送

http2在服务端获取到html文件时就知道需要对应的资源, 因此会直接向客户端推送, 而perload会在浏览器接收到html文件的时候才开始扫描这些预加载文件.

但是HTTP2不能用于第三方的资源推送, 而且preload有益于浏览器确定资源加载的优先级

prefetch

prefetch 是告诉浏览器下一页可能会用到的资源, 用于加速下一个页面的加载速度.

<link rel=“prefetch”>

在vue ssr生成的页面中, 首页的资源都会使用preload, 而路由对应的页面则会有prefetch

注意, prefetch和preload不要混用, 会造成重复加载资源.

字体压缩

这里主要介绍两个工具吧,

一个是font-spider, 可以自动检测网页中引用的字体和文字, 来生成字体文件.

一个是fontmin, 可以将一个字体文件最小化, 比如:

var Fontmin = require('fontmin');

var fontmin = new Fontmin()
    .src('fonts/Microsoft Yahei.ttf') // 设置服务端源字体文件
    .dest('build/fonts') // 设置生成字体的目录
    .use(Fontmin.glyph({ 
        text: '字体压缩', // 设置需要的自己
    }));

fontmin.run(function (err, files) { // 生成字体
    if (err) {
        throw err;
    }
    console.log(files[0]); // 返回生成字体结果的Buffer文件
});

fontmin提供了webpack插件, 详细的使用说明可以参看这里

网络优化

静态资源使用CDN

内容分发网络(CDN)就是一组分布在多个不同地理位置的Web服务器.

CDN原理如下:

  1. 当一个用户访问一个网站的时候, 浏览器要经过DNS解析, 然后浏览器想目标服务器发出IP请求并得到资源
  2. 如果网站部署了CDN, 浏览器进行DNS解析
  3. DNS一次想根服务器, 顶级域名服务器, 权限服务器发出请求, 得到全局负载均衡系统(GSLB)的IP地址
  4. 本地DNS向GSLB发出请求, GSLB根据本地的DNS的IP地址判断用户的位置, 筛选出距离用户比较近的本地负载均衡系统(SLB), 并将该SLB的IP地址作为结果返回给本地的DNS
  5. SLB根据浏览器请求的资源和地址, 选择最优的缓存服务器将内容发回给浏览器
  6. 缓存服务器查看是否命中资源, 如果没有命中, 就向源服务器发送请求, 再发给浏览器并缓存在缓存服务器中

减少HTTP请求(针对http1.1)

一个HTTP请求会消耗比较大的资源, 一旦你body中传输的数据很少, 头部和协议的解析消耗的资源占比就会增加.

我们可以缓存ajax请求, 对重复的请求直接从缓存中获取, 可以减少http的请求数量.

使用Http2

HTTP2相比http1.1有很多的优势, 比如解析速度快, 多路复用, 头部压缩, 能够进行服务端推送, 能够控制流量和优先级

具体协议内容参考: 前端面试常备03:从HTTP1到HTTP3知识大全(长文)

优化Cookie的使用

Cookie的优点是兼容性好, 可以在不出参数的情况下和后台进行数据交互, 比如自动登录

缺点是:

  1. IE老浏览器会显示Cookie的数量
  2. 域名设置不当会导致所有请求都带上Cookie信息
  3. Cookie的读写性能非常的差

优化的方式如下:

  1. 尽可能减少网站中使用的cookie大小
  2. 给cookie设置合理的过期时间
  3. 静态资源不使用cookie, 进行cookie隔离

减少DNS查询

DNS负责将域名URL转化为服务器主机IP

DNS查找流程:

  1. 查看浏览器缓存是否存在
  2. 查看本机DNS缓存
  3. 访问本地DNS服务器
  4. ...

通常浏览器查找一个给定URL的IP地址需要花费20~120ms

TTL(Time to Live) 表示查找返回的DNS记录包含的一个存活之间, 过期则该DNS被抛弃

DNS缓存的TTL值有几个影响因素:

  1. 服务器可以设置TTL表示DNS记录的存活时间. 本机DNS缓存将根据这个TTL值判断DNS记录什么时候被抛弃. TTL不能设置很大, 因为存在快速故障转移的问题
  2. 浏览器DNS缓存也有自己的过期时间, 该事件独立于本机DNS缓存, 相对比较daunt, 例如chrome只有一分钟
  3. 浏览器DNS记录的数量是有限制的, 如果短时间访问大量不同域名的网站, 则比较早的DNS记录会被抛弃

因此, 针对DNS优化, 我们需要恰当的减少主机域名的数量. 但是过少的主机名会限制并行下载的数量(注意: 针对http1.1), 比较恰当的数量是2-4个主机名能够获取加大收益

避免重定向

重定向用于将用户从一个URL重新路由到另一个URL, 一般有:

  1. 301: 永久重定向
  2. 302: 临时重定向
  3. 304: Not Modified

详情参照前端面试常备03:从HTTP1到HTTP3知识大全(长文)

页面发生重定向会延迟真个HTML文档的传输, 增长白屏的时间.

那什么时候会用到重定向呢:

  1. 跟踪内部的流量: 用户离开主页之后的流量, 但最好用内部的referer日志来跟踪内部流量
  2. 跟踪出站的流量: 比如某些链接会出站, 我们可以将其包装在一个302的重定向连接中来解决跟踪的问题

启用 Gzip

Gzip能更进一步的压缩前端资源文件的大小.

但是不是每个浏览器都支持gzip的, 可以在请求头中配置accept-encoding来表示对压缩的支持, 客户端http请求头声明浏览器支持的压缩方式, 服务端配置启用压缩, 压缩的文件类型, 压缩方式, 当客户端请求到服务端的时候, 服务器解析请求头, 如果客户端支持gzip, 响应的时候就会进行资源的压缩并返回给客户端. 浏览器按照自己的方式解析.

如何启用Gzip这里不再赘述

缓存控制

浏览器缓存可以参照前端面试常备06:浏览器缓存

  1. 频繁变动的资源: Cache-Control: no-cache
  2. 不常变化的资源: Cache-Control: max-age=31536000

具体的内容在这里就不展开了

构建优化

目前主流的前端项目都是有构建过程的, 其中有许多优化技巧

tree shaking

tree shaking意为将js文件中用不到的代码在打包的过程中删除.

webpack2以上以及rollup都能很好的支持该特性

无用代码消除广泛存在于编程语言编译器中, 称为DCE(dead code elimination)

其大致的实现原理简单的概括就是:

  1. ES6 Module引入进行静态分析, 编译的时候就可以正确判断加载了哪些模块
  2. 静态分析程序流, 判断哪些模块和变量未被使用或者引用, 进而删除对应的代码

在webpack中, 可以通过在package.json中配置sideEffects表示可以进行treeshaking:

{
  "name": "your-project",
  "sideEffects": false
}

或者表示那些文件具有副作用:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}

而对于rollup, 其默认支持treeshaking

代码压缩

webpack等打包插件可以会生产环境的代码进行压缩, 能够减小资源文件的大小, 优化前端的加载性能.

压缩能力目前都内置在打包工具中, 只要开始生产环境配置即可.

比如webpack配置mode: 'production'就可以开启压缩:

module.exports = {
  mode: 'production'
};

rollup则需要安装对应的插件, 比如terser:

import { terser } from "rollup-plugin-terser";

export default {
  plugins: [
	  terser({ compress: { drop_console: true } })
  ]
};

打包分离(Bundle Splitting)

打包分离的思想是: 如果你有一个体积巨大的文件, 并且只修改了一行代码, 用户却仍然需要重新下载整个文件, 但是如果我把它分成了两个文件, 那么用户只需要下载那个被修改的文件, 而另一个文件直接从缓存中获取就可以了.

从这个角度看, 打包分离与缓存相关, 所以对站点的首次访问者来说没有区别.

webpack可以简单的配置:

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
  },
};

结果会生成一个main.js和一个vendor.js把第三方类库进行分离. 这个配置意味着: 把所有node_modules里的东西都然道verndors~main.js文件中去.

或者我们可以进行这样的配置:

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  plugins: [
    new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
};

这里我们只是简单的介绍打包分离的思想和简单的使用示例, 详细的原理和细节可以参考这篇文章: 深入理解WebPack打包分块

按需加载

按需加载和按需架子啊是两个意图不同的事情, 前者的意图参见上一小节, 按需加载目的是在用户首次访问的时候能尽量减少加载的文件大小, 对暂时不需要用到的代码进行动态加载.

比如这段代码:

window.document.getElementById('btn').addEventListener('click', function () {
  // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
  import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
  })
});

当 Webpack 遇到了类似的语句时会这样处理:

  1. ./show.js为入口新生成一个Chunk
  2. 当代码执行到import所在的语句是会去加载有chunk对应生成的文件
  3. import返回一个promise, 当文件加载成功的时候可以在promisethen方法中获取到show.js导出的内容.

/* webpackChunkName: "show" */ 的含义是为动态生成的 Chunk 赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的 Chunk 的名称,默认名称将会是 [id].js

Dllplugin 提升构建速度

DLLPlugin 和 DLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度。

具体使用参考webpack-DllPlugin

ssr:服务端渲染

目前vue/react生成的前端项目, 其页面视图都是通过js动态生成的, 并且往往前端需要加载一个复杂的rumtime运行库. 对于首屏的渲染来说会比较慢. ssr就是把这部分的渲染过程放在服务端, 请求页面的时候就直接读取dom内容.

不但利于首屏渲染,也有利于SEO

缺点在于会增加服务端的压力, 并且会有一定的改造成本

代码优化

HTML性能优化

HTML的优化主要是规范化标签的使用, 比如:

  1. HTML标签始终闭合
  2. script移到html文件的末尾, 因为JS会阻塞后面的页面的显示
  3. 减少iframe的使用
  4. 简化id和class
  5. 保持统一的大小写
  6. 清除空格
  7. 减少不必要的嵌套
  8. 减少注释
  9. 去除无用的标签和空标签
  10. 减少使用废弃的标签
  11. 避免空的img:src

CSS性能优化

CSS性能是个比较广泛的东西, 主要从四个方面:

  1. 加载性能, 主要是从减少文件体积, 减少阻塞加载, 提高并发出发的
  2. 选择其性能, 但实际上对整体性能的影响忽略不计, selector的考察主要是规范化, 可维护性, 健壮性方面. 可以参考这篇文章: githubs-css-performance
  3. 渲染性能. 渲染性能是css优化的最重要的专注对象. 页面渲染junky过多, 看看是不是使用了text-shadow, 是不是开了字体抗锯齿, CSS动画的实现, 是否合理的使用了GPU加速.
  4. 可维护性, 健壮性. 命名是否合理, 结构层次设计是否健壮, 样式抽象复用了吗
  5. 减少重绘和回流

JS 性能优化

JS性能优化的方向就更多了

从工程角度讲:

  1. 删除没有使用到的功能性代码
  2. 删除多余的依赖库
  3. 删除公共模板代码

从使用内存角度讲:

  1. 数组和对象避免使用构造函数
  2. 避免使用非必要的全局变量
  3. 合理的使用JS缓存机制, 即本地的loaclStorage, SessionStorage, cookie等
  4. 减少循环中的代码实例
  5. 减少比必要的变量声明
  6. 注意闭包的使用, 不要造成内存泄露
  7. 长列表优化
  8. 避免js运行时间过长, 合理的分解任务, 延迟执行高消耗的任务
  9. 利用好web worker, service worker等API
  10. 使用wasm
  11. 使用函数防抖/节流, 尾递归等优化技巧

Vue 性能优化技巧

  1. 使用函数式组件
  2. 子组件拆分
  3. 使用局部变量
  4. v-show代替v-if
  5. 使用keepalive缓存DOM
  6. 使用deferred组件延时分批渲染组件
  7. 使用time slicing时间片切割技术
  8. 合理使用非响应式数据
  9. 使用虚拟滚动组件

......

尾声

性能优化深无止境, 我这篇短文就只能列举其中的部分能用并介绍一二, 甚至不能进行比较深入的讨论, 因为那样就实在太长了. 多积累, 多总结, 总能让技术越来越好的.

参考资料