前端网络层性能优化

前言

在数字时代,速度已成为互联网体验的关键。用户对网页加载时间的容忍度越来越低,每一毫秒的延迟都可能导致用户的流失。根据谷歌的研究,页面加载时间超过3秒的网站,其跳出率会增加120%。在这个以用户为中心的网络世界里,性能优化不再是一个选项,而是必须。

DNS相关优化

用户输入网址后,第一步是解析网址,浏览器需要将这个域名转换为机器可读的IP地址,这是通过DNS(domain name system)查询来完成。

1. DNS缓存

通常我们的操作系统和浏览器已经自动做了相关缓存

  • 浏览器缓存:现代浏览器会缓存DNS查询结果,减少同一域名的重复查询。

  • 操作系统缓存:操作系统也会缓存DNS查询结果,通常可以通过命令查看和管理,如ipconfig /displaydns(Windows)或sudo dscacheutil -cachedump -entries host(macOS)。

  • DNS解析器缓存:本地网络的DNS解析器通常也会缓存查询结果,提高局域网内的查询速度。

2. 减少外部资源引用和开启DNS域名预解析
  • 条件允许的情况下,合并CSS和JS,减少外部文件请求次数

  • 尽可能使用同一个域名下的资源,避免跨域请求

  • 对于跨域资源,可以通过dns-prefetch来开启DNS预解析。

    复制代码
    <link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

    更多关于dns-prefetch的信息请点击这里[1]

3. 启用CDN加速

CDN通过在全球分布的服务器缓存内容,提供更快速的访问。CDN服务提供商通常也提供优化的DNS服务,将用户的DNS查询定向到离用户最近的服务器节点,进一步减少延迟。

HTTP相关优化

解析完域名之后,自然就是开始HTTP连接。在这个阶段,我们能做的事情变得更多了。

1. 关于http协议

http协议总是在不断的升级优化中,更高版本的http协议总能带来更好的性能,根据你项目与用户的实际情况,尽可能的升级到合理的版本。这里不赘述http1/2/3之间的区别,想要了解的出门左转问问GPT。

1.1 http1.1的优化

如果你评估了你的项目确实无法升级到http2或更高,那么可以考虑以下优化

  • 减少请求次数:举个例子,webpack中有个SplitChunksPlugin的插件, 用于分割代码生成产物的,我们可以利用这个插件控制我们的生成物,例如像Vue、Vue-router、Vuex、以及我们用到的一些基础依赖都打包成一个js,对于仅在某些页面引用的依赖进行单独拆分便于按需加载。其他的还有使用雪碧图、将小图片转为base64内嵌到js或html里等手段。

  • 持久连接(Keep-Alive):确保服务器和客户端都支持持久连接,允许在同一个TCP连接上进行多次请求和响应。

  • 设置缓存头:合理设置HTTP缓存头(如Cache-Control、Expires、ETag),利用浏览器缓存减少对相同资源的重复请求。(这不限于http1.1)

  • 在HTTP/1.x中,大多数浏览器对单个域名最多只能同时打开6-8个TCP连接。为了提高并发加载资源的能力,可以将资源分布到多个子域名上,每个子域名可以独立建立连接,从而增加总的并发连接数。

1.2 http2的优化
  • 在HTTP/2中,通过二进制分帧和多路复用技术,可以在同一个TCP连接上并行传输多个请求和响应。所以与http1.1不太一样的,我们应该对js、css进行更加细致的切割,方便我们更加精细的控制按需加载,减少需要加载的文件大小,提高首屏速度。

  • 服务器推送:利用HTTP/2的服务器推送(Server Push)功能,服务器可以主动向客户端推送资源,减少客户端请求的次数。例如,在Nginx中配置:

    复制代码
    http {
      server {
          location / {
              http2_push_preload on;
              add_header Link "<https://example.com/style.css>; rel=preload; as=style";
          }
      }
    }
  • 由于域名分片是有一定代价的(dns查询,tcp链接建立、缓存策略的效率等),http2支持多路复用,已经无需域名分片了,如果有使用域名分片,在升级到http2以后尽可能的将资源移至同一域名下。

2. 请求包大小优化

显而易见,减少我们请求的资源的总大小是一个非常直观的优化指标。

2.1 启用传输压缩算法

尽管nginx等服务端程序提供了像gzipbrotli等压缩功能,但是这会带来不小的服务端开销,我们可以提前在代码打包时进行压缩。

  • webpack需要安装CompressionWebpackPlugin[2]这个插件

    复制代码
    const CompressionPlugin = require('compression-webpack-plugin');
    
    module.exports = {
      // 其他配置项...
      plugins: [
        // Gzip 压缩
        new CompressionPlugin({
          algorithm: 'gzip',
          filename: '[path][base].gz',
          test: /.(js|css|html|svg)$/,
          threshold: 10240,
          minRatio: 0.8
        }),
        // Brotli 压缩
        new CompressionPlugin({
          algorithm: 'brotliCompress',
          filename: '[path][base].br',
          test: /.(js|css|html|svg)$/,
          compressionOptions: { level: 11 },
          threshold: 10240,
          minRatio: 0.8
        })
      ],
    };
  • vite需要安装vite-plugin-compression[3]这个插件

    复制代码
    import { defineConfig } from 'vite';
    import compression from 'vite-plugin-compression';
    
    export default defineConfig({
      plugins: [
        // Gzip 压缩
        compression({
          algorithm: 'gzip',
          ext: '.gz', // 输出的文件名后缀
          threshold: 10240, // 只处理比这个字节大的文件
          deleteOriginFile: false // 是否删除源文件
        }),
        // Brotli 压缩
        compression({
          algorithm: 'brotliCompress',
          ext: '.br', // 输出的文件名后缀
          threshold: 10240, // 只处理比这个字节大的文件
          deleteOriginFile: false // 是否删除源文件
        })
      ]
    });
2.2 按需加载
  • 代码分割(Code Splitting):webpack和vite都是使用import()动态导入语法,轻松实现代码分割,实现按需加载。如果需要更加精确的控制代码分割,还可以使用SplitChunksPlugin(webpack)output.manualChunks(rollup/vite)

  • 单页应用的路由懒加载

    react:

    复制代码
    const LazyComponent = React.lazy(() => import('./LazyComponent'));
    
    function App() {
      return (
        <Suspense fallback={<div>Loading...</div>}>
          <LazyComponent />
        </Suspense>
      );
    }

    vue:

    复制代码
    const routes = [
      {
        path: '/about',
        component: () => import('./components/About.vue')
      }
    ];
  • 图片懒加载:

    • 原生懒加载:使用 loading="lazy" 属性可以原生支持图片和 iframe 的懒加载

      复制代码
      <img src="large-image.jpg" loading="lazy" alt="Lazy Loaded Image">
    • Intersection Observer API:用于懒加载图片、视频、iframe 等资源。

      复制代码
      const imgObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
          }
        });
      });
      
      document.querySelectorAll('img[data-src]').forEach(img => {
        imgObserver.observe(img);
      });
  • 按需加载第三方库:例如lodash替换为lodash-es, 像 element-ui等UI库使用按需导入组件的方式去使用。

3. HTTP缓存
3.1 强缓存
  • Expires: 定义资源过期的具体时间点。浏览器会在该时间点之前使用缓存的资源,而不访问服务器。缺点是由于使用的是具体的时间戳,可能会受到客户端时间不准确的影响

    复制代码
    Expires: Wed, 21 Oct 2024 07:28:00 GMT
  • Cache-Control:优先级高于 Expires 头。它可以通过多种指令控制缓存行为,包括设置缓存的最大有效期(max-age)、指定资源是公共还是私有缓存(public/private)、禁止缓存(no-cache)、以及禁止存储(no-store)等。

    复制代码
    Cache-Control: max-age=31536000
3.2 协商缓存
  • Last-ModifiedIf-Modified-Since: 首次访问时返回的请求头Last-Modified代表资源最后修改时间,后续请求会作为If-Modified-Since的值发送到服务端,服务端会通过判断这个值与资源最后修改时间是否一致,如果一致则返回 304 NOT_MODIFY,不传输资源内容,否则返回200并传输资源内容。

  • ETagIf-None-Match: etag是服务端生成的唯一标识符(通常是资源的哈希值),后续请求中作为If-None-Match的值发送到服务端,服务端判断资源是否修改。由于Last-Modified基于时间并且精度为秒,所以可能没那么精确;etag更加精确但是由于是实时计算资源的哈希值,所以服务端压力更大。

3.3 强缓存与协商缓存的配合使用

上述两种缓存方式并不是互斥选项,可以配合使用。例如配置一定时间的强缓存,强缓存失效后进行协商缓存查看资源是否修改,如无修改可以继续使用缓存资源。

复制代码
Cache-Control: max-age=31536000
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

// 后续请求
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
3.4 service worker

Service Worker 是一种在浏览器后台运行的脚本,可以拦截和处理网络请求,从而实现缓存和离线访问等功能。通过 Service Worker,你可以控制资源的缓存策略,包括静态资产和动态请求。

4. 浏览器缓存
4.1 cookies

Cookies 是一种在客户端(浏览器)存储小量(一般是4KB)数据的机制,用于在多个请求和页面访问之间维持状态信息。它们在 Web 开发中被广泛使用,可以存储用户的会话信息、用户偏好设置、跟踪用户行为等。

4.2 web storage

是浏览器提供的两组api localStoragesessionStorage,用于存储键值对,大小通常为5M~10M。不同点在于

  • localStorage为永久存储,不主动删除不会消失,sessionStorage会随会话销毁而消失

  • localStorage可以跨窗口使用,sessionStorage不行

4.3 IndexedDB

IndexedDB 是一种浏览器内建的 NoSQL 数据库,允许在客户端存储大量结构化的数据。与 Web Storage API(localStorage 和 sessionStorage)相比,IndexedDB 提供了更多的功能和灵活性,适合存储复杂数据和执行更复杂的查询操作。

  • 存储容量更大,根据不同浏览器的实现,通常有几百M到上G

  • 支持更多的数据格式例如对象、数组等,不限于字符串

  • 支持事务、索引等

  • 异步API,不阻塞渲染,通过promise返回

本文转载于稀土掘金技术社区,作者:翔啊翔阿翔

原文链接:https://juejin.cn/post/7402204612143497226

相关推荐
10年前端老司机8 分钟前
什么!纯前端也能识别图片中的文案、还支持100多个国家的语言
前端·javascript·vue.js
摸鱼仙人~11 分钟前
React 性能优化实战指南:从理论到实践的完整攻略
前端·react.js·性能优化
程序员阿超的博客1 小时前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 2451 小时前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
小小小小宇6 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖6 小时前
http的缓存问题
前端·javascript·http
小小小小宇7 小时前
请求竞态问题统一封装
前端
loriloy7 小时前
前端资源帖
前端
源码超级联盟7 小时前
display的block和inline-block有什么区别
前端
GISer_Jing7 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js