前端性能优化

Posted by violetks on April 1, 2023

一、感知性能优化

让用户感觉你的网站访问很快,没有衡量标准。如果一个页面的加载时间很长,可以通过一些方式让用户觉得没有那么慢。

1、使用 loading 图标、骨架屏

二、HTML 优化

1、压缩 HTML

HTML 代码压缩,将注释、空格和新行从生产文件中删除。删除所有不必要的空格、注释和中断行将减少 HTML 的大小,加快网站的页面加载时间,并显著减少用户的下载时间。

2、删除不必要的注释

注释对用户来说是没有用的,应该从生产环境文件中删除。可能需要保留注释的一种情况是:保留远端代码库(keep the origin for a library)。
可以使用 HTML minify 插件删除注释。
remove-html-comments - npm

3、删除不必要的属性

type="text/javascript"type="text/css"这样的属性应该被移除,因为 HTML5 把text/csstext/javascript作为默认值。

<!-- Before HTML5 -->
<script type="text/javascript">
    // Javascript code
</script>

<!-- Today -->
<script>
    // Javascript code
</script>

4、使用语义化标签

语义化优势:
(1)易于用户阅读,样式丢失的时候能让页面呈现清晰的结构。
(2)有利于 SEO,搜索引擎根据标签来确定上下文和各个关键字的权重。
(3)方便其他设备解析,如盲人阅读器根据语义渲染网页。
(4)有利于开发和维护,语义化更具可读性,代码更好维护,与 CSS3 关系更和谐。

5、减少 iframe 数量

尽量少用 iframe 标签,爬虫是不会读取 iframe 的内容的。

6、减少 DOM 数量和层级数量

HTML 中标签元素越多,标签的层级越深,浏览器解析 DOM 并制作到浏览器中所花的时间就越长。

7、减少 HTTP 请求次数

将多个 CSS 和 JavaScript 文件合并为一个文件,可以减少 HTTP 请求次数,从而提高页面加载速度。同时,使用浏览器缓存可以避免每次请求相同的文件。

8、减少重排重绘

三、JavaScript 优化

1、压缩代码

2、删除重复的脚本

一个页面中如果有两次使用到同一个 JavaScript 文件,会增加不必要的 HTTP 请求,增加 JavaScript 执行时间。

3、JavaScript 脚本放到页面底部

脚本的下载和执行,会阻塞其他资源(样式文件或图片)的下载。因此,将<script>标签尽量放到<body>标签的底部。

4、从页面中分离 JavaScript 代码,从外部引入

5、减少 DOM 操作

6、防抖和节流

频繁调用函数很可能会造成页面的卡顿,可以通过防抖和节流,限制函数的执行频率,避免过多的重复操作。

7、使得 Ajax 可缓存

POST 的请求,是不可以在客户端缓存的,每次请求都需要发送给服务器进行处理,每次都会返回状态码 200。(可以在服务器端对数据进行缓存,以便提高处理速度)。
GET 的请求,是可以(而且默认)在客户端进行缓存的,除非指定了不同的地址,否则同一个地址的 AJAX 请求,不会重复在服务器执行,而是返回 304。

8、长列表虚拟滚动优化

虚拟列表是一种用来优化长列表的技术。它可以保证在列表元素不断增加,或者列表元素很多的情况下,依然拥有很好的滚动、浏览性能。它的核心思想在于:只渲染可见区域附近的列表元素

四、CSS 优化

1、CSS 文件压缩

通过压缩 CSS 文件大小来提高页面加载速度。现在的构建工具,如 Webpack、Gulp/Grunt、Rollup 等也都支持 CSS 压缩功能。压缩后的文件能够明显减小,可以降低浏览器的加载时间。

2、删除无用的 CSS,精简代码,应该尽可能地提取公共类,减少重复。

3、删除不必要的单位和零

4、将样式表放到页面顶部

我们希望浏览器尽早渲染获取到的任何内容。这对大页面和网速慢的用户很重要。给用户视觉反馈,比如进度条的重要性已经被大量研究和记录。在我们的情况中,HTML 页面就是进度条。当浏览器逐步加载页面头部,导航条,logo 等等,这些都是给等待页面的用户的视觉反馈。这优化了整体用户体验。

5、从页面中分离 CSS 代码,从外部引入

6、页面导入样式时,尽量少用 @import

(1)使用 @import 引入 CSS 会影响浏览器的并行下载。使用 @import 引用的 CSS 文件只有在引用它的那个 CSS 文件被下载、解析之后,浏览器才会知道还有另外一个 CSS 需要下载,这时才去下载,然后下载后开始解析、构建 render tree 等一系列操作。这就导致浏览器无法并行下载所需的样式文件。
(2)多个 @import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在 @import 后面的 JS 文件先于 @import 下载,并且打乱甚至破坏 @import 自身的并行下载。

7、避免 !important

因为这破坏了样式表中固有的级联规则,使调试 bug 变得更加困难。它很容易使用不当,而且容易成倍增加,尤其是在滥用时。您可以轻松地 !important 得出一个带有要覆盖的规则的元素,这时您通常不得不重构样式,或使用另一个 !important 规则来加剧问题。
如果你一定要使用,最好定义基本样式尽可能靠近 html 或 body 元素,并且要覆盖时,请尽量避免使用特殊性。这样,您就有足够的空间进行更改。

8、不要在 ID 选择器前面进行嵌套其他选择器

(1)ID 选择器本来就是唯一的而且人家权值那么大,前面嵌套,完全是浪费性能。
(2)除了嵌套,在 ID 选择器前面也不需要加标签或者其它选择器。比如 div#dom 或者 .box#dom。这两种方式完全是多余的,理由就是 ID 在页面就是唯一的。前面加任何东西都是多余的!

9、CSS 层级嵌套最好不超过三层

过度的嵌套会导致代码变得臃肿,沉余,复杂。导致 CSS 文件体积变大,造成性能浪费,影响渲染的速度。而且过于依赖 HTML 文档结构。这样的 CSS 样式,维护起来极度麻烦,如果以后要修改样式,可能要使用 !important 覆盖。

10、慎用 * 通配符

11、异步加载非首屏 CSS

CSS 会阻塞 DOM 的渲染,所以我们将首屏关键 CSS 内联后,剩余的非首屏 CSS 内容可以使用外部 CSS,并且异步加载,防止非首屏 CSS 内容阻塞页面的渲染。

五、图片优化

1、压缩图片

压缩图片可以减少图片的文件大小,从而减少加载时间。

2、小图片使用雪碧图

雪碧图(CSS Sprite)又叫 CSS 精灵图主要用于把一堆小图标整合在一张背景透明的大图上,通过设置对应的位置来显示不同的图片,目的是大幅减轻服务器对图片的请求数量。
优势:
(1)减少网页的 HTTP 请求,提高页面性能。
(2)减少图片命名的困扰。
劣势:
(1)需要计算每个图片的位置。
(2)后期维护困难。

3、图片懒加载

一般来说,我们访问网站页面时,其实很多图片并不在首屏中,如果我们都加载的话,相当于是加载了用户不一定会看到图片,这显然是一种浪费。解决的核心思路就是懒加载:先不给图片设置路径,当图片出现在浏览器可视区域时才设置真正的图片路径。

4、缩小 favicon.ico 并缓存

有利于 favicon.ico 的重复加载,一般一个 Web 应用的 favicon.ico 是很少改动的。

5、img 图片的 alt 属性要写,合理使用 target=”_blank”

合理而不频繁使用 target=”_blank” 是可以在一定程度上为站点带来回旋流量和点击的,可以增强站点总体用户体验。

6、使用 SVG 图片或者字体图标

因为字体图标或者 SVG 是矢量图,代码编写出来的,放大不会失真,而且渲染速度快。字体图标使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便,还有一个优点是生成的文件特别小。

7、使用 WebP 格式图片

WebP 是一种由 Google 开发的图片格式,可以大幅减少文件大小,从而减少加载时间。WebP 支持无损、有损压缩,动态、静态图片,压缩比率优于 GIF、JPEG、JPEG2000、PNG 等格式,非常适合用于网络等图片传输。

8、使用恰当的图片格式

使用恰当的图片格式可以大大减少图片的文件大小。对于需要透明背景的图片,使用 PNG 格式;对于颜色较少的图像,使用 GIF 格式;对于照片和复杂图像,使用 JPEG 格式。

9、移除图片元数据

图片通常包含元数据,例如拍摄日期、相机型号等。移除这些元数据可以减少图片的文件大小。

10、按照 HTTP 协议设置合理的缓存

将图片缓存在浏览器中可以减少页面加载时间。你可以使用浏览器缓存机制或者使用 CDN 来缓存图片。

11、用 CSS 或 JavaScript 实现预加载

12、不要在 HTML 中使用缩放图片

13、不用图片,尽量用 CSS 代替

14、Base64 格式

将图片的内容以 Base64 格式内嵌到 HTML 中,可以减少 HTTP 请求数量。但是,由于 Base64 编码用 8 位字符表示信息中的 6 个位,所以编码后大小大约比原始值扩大了 33%。

六、Webpack 构建优化

1、线程加载器

多线程可以提高程序的效率,我们也可以在 Webpack 中使用。而 thread-loader 是一个可以在 Webpack 中启用多线程的加载器。
安装:

npm i thread-loader -D

webpack.config.js 中配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          {
            loader: "thread-loader",
            options: {
              workers: 2,
              workerParallelJobs: 50,
              workerNodeArgs: ['--max-old-space-size', '1024'],
              poolTimeout: 2000,
              poolParallelJobs: 50
            }
          },
          "expensive-loader"
        ]
      }
    ]
  }
}

注意:
不一定使用该 Loader 就一定能提升效率,因为进程之间的通信也会消耗时间,一般在处理文件比较耗时的 Loader 后面使用该 Loader。

2、缓存加载器

在我们的项目开发过程中,Webpack 需要多次构建项目。为了加快后续构建,我们可以使用缓存,与缓存相关的加载器是缓存加载器。
安装:

npm i cache-loader -D

Webpack 的配置如下:

module.exports = {
    module: {
        // 如果babel-loader耗时比较长,配置cache-loader
        rules: [
            {
                test: /\.jsx?$/,
                use: ['cache-loader','babel-loader']
            }
        ]
    }
}

3、Hot update 热更新

当我们在项目中修改一个文件时,Webpack 默认会重新构建整个项目,但这并不是必须的。我们只需要重新编译这个文件,效率更高,这种策略称为 Hot update。Webpack 内置了 Hot update 插件,我们只需要在配置中开启 Hot update 即可。
Webpack 的配置如下:

const webpack = require('webpack');
module.exports = {
    module: {
      plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        hot: true
    }
  }
}

4、exclude & include

一些文件和文件夹永远不需要参与构建。所以我们可以在配置文件中指定这些文件,防止 Webpack 取回它们,从而提高编译效率。
(1)exclude:不需要编译的文件。
(2)include:需要编译的文件。

// webpack.config.js
const path = require('path');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: ['babel-loader'],
                include: [path.resolve(__dirname, 'src')]
            }
        ]
    }
}

5、通过 Webpack 插件压缩 CSS 代码

css-minimizer-webpack-plugin 可以压缩和去重 CSS 代码。
安装:

npm i css-minimizer-webpack-plugin -D

webpack.config.js 中配置:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /.s?css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
  optimization: {
    minimizer: [
      // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

这将仅在生产环境开启 CSS 优化。
如果还想在开发环境下启用 CSS 优化,请将 optimization.minimize 设置为 true:

module.exports = {
  optimization: {
    // [...]
    minimize: true,
  },
};

6、通过 Webpack 插件压缩 JavaScript 代码

terser-webpack-plugin 可以压缩和去重 JavaScript 代码。
安装:

npm i terser-webpack-plugin -D

webpack.config.js 中配置:

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          ecma: undefined,
          parse: {},
          compress: {},
          mangle: true,
          module: false,
          output: null,
          format: null,
          toplevel: false,
          nameCache: null,
          ie8: false,
          keep_classnames: undefined,
          keep_fnames: false,
          safari10: false
        }
      })
    ]
  }
};

7、tree-shaking

作用是把 JS 文件中无用的模块或者代码删掉。而这通常需要借助一些工具。在 Webpack 中 tree-shaking 就是在打包时移除掉 Javascript 上下文中无用的代码,从而优化打包的结果。在 Webpack5 中已经自带 tree-shaking 功能,在打包模式为 production 时,默认开启 tree-shaking 功能。

module.exports = {
  mode: 'production'
}

8、source-map

当我们的代码出现 bug 时,source-map 可以帮助我们快速定位到源代码的位置。但是这个文件很大。因此根据不同的环境来配置。
开发模式:
生成更准确(但更大)的 source-map。

module.exports = {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map'
}

生产模式:
生成更小(但不那么准确)的源映射。

module.exports = {
  mode: 'production',
  devtool: 'nosources-source-map'
}

9、Bundle Analyzer

我们可以使用 webpack-bundle-analyzer 来查看打包后的 bundle 文件的体积,然后进行相应的体积优化。
安装:

npm i webpack-bundle-analyzer -D

1、webpack.config.js配置:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()  // 使用默认配置
    // 默认配置的具体配置项
    // new BundleAnalyzerPlugin({
    //   analyzerMode: 'server',
    //   analyzerHost: '127.0.0.1',
    //   analyzerPort: '8888',
    //   reportFilename: 'report.html',
    //   defaultSizes: 'parsed',
    //   openAnalyzer: true,
    //   generateStatsFile: false,
    //   statsFilename: 'stats.json',
    //   statsOptions: null,
    //   excludeAssets: null,
    //   logLevel: info
    // })
  ]
}

2、配置 package.json 文件

{
 "scripts": {
    "dev": "webpack --config webpack.dev.js --progress"
}

3、命令启动

npm run dev

看到项目资源包的交互式可视化树形分析图。

bundle.gif

10、模块懒加载

如果模块没有延迟加载,整个项目的代码会被打包成一个 JS 文件,导致单个 JS 文件体积非常大。那么当用户请求网页时,首屏的加载时间会更长。
使用模块来加载后,大 JS 文件会被分割成多个小 JS 文件,加载时网页按需加载,大大提高了首屏的加载速度。

const routes = [
  { // 未使用懒加载
    path: '/index',
    name: 'index,
    component: index
  },
  {
    path: '/detail',
    name: 'detail',
    // 启用懒加载
    component: () => import('../views/detail/detail.vue'),
  },
]

11、压缩包

Gzip 是一种常用的文件压缩算法,可以提高传输效率。但是,此功能需要后端配合。
安装:

npm i compression-webpack-plugin -D

修改vue.config.js配置:

const CompressionWebpackPlugin = require('compression-webpack-plugin');
const isProdOrTest = process.env.NODE_ENV !== 'development'
module.exports = {
    productionSourceMap: false,// 设为 false,既可以减少包大小,也可以加密源码
    transpileDependencies: ['element-ui'],//指定某个库在打包的时候需要编译
    chainWebpack(config) {
        config.plugins.delete('prefetch');//默认开启 prefetch(预先加载模块),提前获取用户未来可能会访问的内容 在首屏会把这十几个路由文件,都一口气下载了 所以我们要关闭这个功能模块
        if (isProdOrTest) {
            // 对超过 10kb 的文件 gzip 压缩
            config.plugin('compressionPlugin').use(
                new CompressionWebpackPlugin({
                    test: /\.(js|css|html)$/,// 匹配文件名
                    threshold: 10240,
                })
            );
        }
    }
}

同时需要 nginx 开启 gzip 压缩功能(nginx 配置参考服务器优化 gzip 压缩配置)。

12、Base64

对于一些小图片,可以转成 base64 编码,这样可以减少用户的 HTTP 请求次数,提升用户体验。url-loader 在 webpack5 中已被弃用,我们可以使用 assets-module 代替。顺便也可以添加一些字体图片的配置、音频配置、Eslint 配置、babel 配置。

npm i eslint-webpack-plugin eslint -D

npm i babel-loader @babel/core @babel/preset-env -D
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    clean: true, // 自动将上次打包目录资源清空
  },
  presets: ["@babel/preset-env"],
  module: {
    rules: [
      {
        // 用来匹配 .css 结尾的文件
        test: /\.css$/,
        // use 数组里面 Loader 执行顺序是从右到左
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
      {
        test: /\.s[ac]ss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.styl$/,
        use: ["style-loader", "css-loader", "stylus-loader"],
      },
      {
        test: /\.(png|jpe?g|gif|webp)$/,
        type: "asset",
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于 10kb 的图片会被 base64 处理
          },
        },
        generator: {
          // 将图片文件输出到 static/imgs 目录中
          // 将图片文件命名 [hash:8][ext][query]
          // [hash:8]: hash 值取 8 位
          // [ext]: 使用之前的文件扩展名
          // [query]: 添加之前的 query 参数
          filename: "static/imgs/[hash:8][ext][query]",
        },
      },
      {
        test: /\.(ttf|woff2?)$/,
        type: "asset/resource",
        generator: {
          filename: "static/media/[hash:8][ext][query]",
        },
      },
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除 node_modules 代码不编译
        loader: "babel-loader",
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "src"),
    }),
  ],
  mode: "development",
};

13、正确配置哈希

我们可以将哈希添加到捆绑文件中,这样可以更轻松地处理缓存。

output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'js/chunk-[contenthash].js',
    clean: true,
}

七、资源加载优化

1、使用 Web Workers

Web Worker 是一个独立的线程(独立的执行环境),这就意味着它可以完全和 UI 线程(主线程)并行的执行 JS 代码,从而不会阻塞 UI,它和主线程是通过 onmessage 和 postMessage 接口进行通信的。
Web Worker 使得网页中进行多线程编程成为可能。当主线程在处理界面事件时,worker 可以在后台运行,帮你处理大量的数据计算,当计算完成,将计算结果返回给主线程,由主线程更新 DOM 元素。

2、DNS 预解析

浏览器对网站第一次的域名DNS解析查找流程依次为:

浏览器缓存 ->系统缓存 ->路由器缓存 ->ISP DNS 缓存 ->递归搜索

DNS 预解析的实现:
用 meta 信息来告知浏览器,当前页面要做 DNS 预解析:

<meta http-equiv="x-dns-prefetch-control" content="on" />

在页面 header 中使用 link 标签来强制对 DNS 预解析:

<link rel="dns-prefetch" href="https://code-nav.top" />

dns-prefetch 最大的缺点就是使用它太多。过多的预获取会导致过量的 DNS 解析,对网络是一种负担。

3、预加载 preload

(1)遇到 link 标签时,立刻下载并放到内存中,不执行 JS。
(2)遇到 script 标签时,将预加载的 JS 执行。
(3)对跨域的文件进行 preload 时,必须加上 crossorigin 属性。

<link rel="preload" crossorigin href="./zone.js" as="script">

基于标记语言的异步加载:

<link rel="preload" as="style" href="asyncstyle.css" onload="this.rel='stylesheet'">

八、浏览器缓存策略

缓存是浏览器的一种机制,可以把请求过的 Web 资源(HTML、CSS、JS、图片等)拷贝一份副本存储在浏览器中,并根据请求配置选择是否使用该副本。缓存是浏览器中一种重要的并且简单高效的一种性能优化的方式。一个优秀的缓存策略起到以下作用:
(1)缩短网页请求资源的距离,减少延迟,增加用户体验。
(2)缓存文件可以重复利用,还可以减少带宽,降低网络负荷。
(3)降低服务器压力。

1、缓存位置

(1)Service Worker
Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

(2)Memory Cache
Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存。

memory.png

注意不能让数据都存放在内存中。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说 JSS、HTML、CSS、图片等等。但是浏览器会把哪些文件丢进内存这个过程没有一个定论。对于大文件来说,大概率是不存储在内存中的,反之优先。当前系统内存使用率高的话,文件优先存储进硬盘。

(3)Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。

(4)Push Cache
Push Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。
Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及,但是 HTTP/2 将会是日后的一个趋势。

  • 所有的资源都能被推送,但是 Edge 和 Safari 浏览器兼容性不怎么好
  • 可以推送 no-cache 和 no-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

2、缓存策略

通常浏览器缓存策略分为两种:强缓存协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
(1)协商缓存:
如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
协商缓存.png

Last-Modified 和 If-Modified-Since
Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。
但是 Last-Modified 存在一些弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源。
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源。

因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag 。

ETag 和 If-None-Match
ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。

如果什么缓存策略都没设置,那么浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

(2)强缓存:
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200。

Expires

Expires: Wed, 22 Oct 2022 08:41:00 GMT

Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-Control

Cache-control: max-age=30

Cache-Control 出现于 HTTP/1.1,优先级高于 Expires。该属性值表示资源会在 30 秒后过期,需要再次请求。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令。

cache-control.png

从图中看出,可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。

cache指令.png

3、缓存场景

(1)频繁变动的资源
对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。
(2)代码文件
这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。
一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

4、用户操作行为与缓存

操作行为.png

主要有 3 种:

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

九、服务器优化

1、静态资源使用 CDN

用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面,CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
静态资源使用CDN.png

2、添加 Expires 或者 Cache-Control 响应头

使用 cach-control 或 expires 这类强缓存时,缓存不过期的情况下,不向服务器发送请求。强缓存过期时,会使用 last-modified 或 etag 这类协商缓存,向服务器发送请求,如果资源没有变化,则服务器返回 304 响应,浏览器继续从本地缓存加载资源;如果资源更新了,则服务器将更新后的资源发送到浏览器,并返回 200 响应。

3、对组件使用 Gzip 压缩

在服务器端对文件进行紧缩,在浏览器端对文件解紧缩,可有用削减通讯传输的数据量。
在nginx上配置开启:

http {
    gzip on;
    gzip_static on;
    gzip_min_length 1k;
    gzip_buffers 8 16k;
    gzip_http_version 1.1;
    gzip_comp_level 6;
    gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml image/png image/gif image/jpeg image/ico image/jpg;
    gzip_vary on;
    gzip_proxied expired no-cache no-store private auth;
    gzip_disable "MSIE [1-6]\.";
    limit_conn_zone $binary_remote_addr zone=perip:10m;
    limit_conn_zone $server_name zone=perserver:10m;
}

在控制台查看到请求头有 gzip 配置成功,如下图。
gzip配置.png

4、配置 ETag

Entity tags(ETags)(实体标签)是 Web 服务器和浏览器用于判断浏览器缓存中的内容和服务器中的原始内容是否匹配的一种机制(“实体”就是所说的“内 容”,包括图片、脚本、样式表等)。增加 ETag 为实体的验证提供了一个比使用“last-modified date(上次编辑时间)”更加灵活的机制。Etag 是一个识别内容版本号的唯一字符串。唯一的格式限制就是它必须包含在双引号内。原始服务器通过含有 ETag 文件头的响应指定页面内容的ETag。

HTTP/1.1 200 OK

Last-Modified: Tue, 12 Dec 2023 03:03:59 GMT

ETag: “10c24bc-4ab-457e1c1fd”

Content-Length: 1323

5、提供来自相同协议的文件

避免网站使用 HTTPS 同时使用 HTTP 来提供相同源地址的文件。

6、开启 HTTP2(多路复用,并行加载)

HTTP2 带来了非常大的加载优化,所以在做优化上首先就想到了用 HTTP2 代替 HTTP1。
优点:

  • 解析速度快
  • 多路复用
  • 请求头压缩
  • 服务器推送

在 nginx 上配置开启:
配置 HTTP2,首先要添加 SSL 证书,添加完证书后,在端口后面添加 HTTP2 即可。

server {
        listen 80;
        #开启http2
        listen 443 ssl http2;
        server_name  code-nav.top;
        #配置证书
        ssl_certificate /www/nginx/cert/code-nav.top.pem;
        ssl_certificate_key  /www/nginx/cert/code-nav.top.key;
        ssl_session_timeout 5m;
        #请按照以下协议配置
        ssl_protocols TLSv1.2 TLSv1.3;
        #请按照以下套件配置配置加密套件写法遵循 openssl 标准
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
        ssl_prefer_server_ciphers on;
        location / {
            proxy_pass https://myblog;
            proxy_set_header Host $proxy_host;
        }

        access_log /www/wwwlogs/access.log;
}

在控制台查看到 HTTP2 配置成功,如下图。

http2配置.png

7、服务端渲染

服务端返回 HTML 文件,客户端只需解析 HTML。首屏渲染快,SEO 好。
缺点:配置麻烦,增加了服务器的计算压力。

8、分域存放资源

由于浏览器同一域名并行下载数有限,利用多域名主机存放静态资源,增加并行下载数,缩短资源加载时间。

9、减少页面重定向

十、Vue 优化

1、使用 v-if 和 v-show 判断是否渲染元素。
2、避免在列表渲染中使用 v-if。
3、使用计算属性(computed properties)缓存计算结果。
4、使用 key 来帮助 Vue 识别列表中各项的唯一性,优化 DOM diff。
5、避免在 data 对象中使用响应式属性,如果不会改变,则应设为 data 对象外的单独变量。
6、使用 Object.freeze() 来冻结不会改变的响应式对象。
7、使用 v-for 时提供 :key。
8、避免在模板中使用内联表达式,应该使用计算属性。
9、使用 keep-alive 缓存组件。
10、路由懒加载。
11、渲染大型列表时,通过列表虚拟化来提升性能,只需要渲染用户视口中能看到的部分。

十一、性能测试网站推荐

1、WebPageTest

在线web性能测试工具,提供多地点测试。他只能测试已经发布了的网站。输入需要测试的网页地址,点击 start test 按钮就开始测试了,可以选择测试地理位置,测试的浏览器等。
WebPageTest.png

2、Web Page Analyzer

它是一种强大的速度测试工具,为您提供了详细现场分析以及如何改进的建议。它提供许多功能包括网页速度报告,全球报告,外部文件计算,加载时间,分析和建议等。

3、GT Metrix

GT Metrix 可以帮助您开发更快,更高效的网站体验,能全方位改善网站体验。它对你的网站性能继续分级,并提供可行的建议,以解决发现的任何问题。

4、Pingdom

Pingdom 是一个非常棒的工具,它可以帮助用户通过生成大量报道,如页面大小,浏览器缓存,性能等级等等来确定网站的加载时间。它允许您从不同的位置跟踪历史表现和行为测试。可以设置每隔几分钟测试你的网站,每周发送邮件总结一周内你的网站平均加载时间,可以用来实时监控网站的响应性能。

5、36全球网站性能测试(强烈推荐)

36全球网站性能测试.png

工具一开始是起飞页和虎翼商城的内部测试工具,专门用来测试网站的国外访问速度。虽然国内也不乏其它可以测试网站速度的工具,但他们都只能给出简单的CURL和WGET的时间,并没有详细的渲染时间和可交互时间,更没有SEO评测,可用性评测。