95%开发者都踩过的坑:前端性能优化的逆向思维指南

前言

性能优化,一个掣肘用户体验的关键模块。它没有固定的标准定义或唯一的解决方案。但是我们从整个项目的开发-部署-用户体验的整个过程中,总能摸到很多有普适性的规范或者优化理念。

我们的目的是什么?优化啥?

应当是:更快的加载和响应速度、更稳定的功能表现、更简洁的代码与架构设计、更好用更人性化。

说人话是:性能优化应当是让用户能感觉到爽的,并且产生用户粘性的所有方式的总称

好吧,也没有那么人话…

那知道了我们性能优化的目的,我们要如何搞,才可以达到这个目的呢?

在我的脑图中,我一直将性能优化分为2大模块:

  • 针对网络层面的优化,
  • 针对渲染层面的优化。

这是从浏览器的渲染流程来进行分类的(老生常谈了,知道的可以跳过)

当用户在浏览器地址栏输入网址并按下回车后,浏览器首先通过DNS解析获取域名对应的IP地址,然后建立TCP连接(或HTTPS需额外进行TLS握手)。接着,浏览器向服务器发送HTTP请求,服务器处理请求并返回HTML等响应内容。浏览器接收响应后开始解析HTML,构建DOM树,同时解析CSS构建CSSOM树,并通过JavaScript处理动态逻辑。随后,浏览器结合DOM树和CSSOM树生成渲染树,进行布局计算确定元素的大小和位置,最终将内容绘制到屏幕上。整个过程中,外部资源如CSS、JS、图片等会并行加载,并在加载完成后动态更新页面。渲染完成后,浏览器持续加载异步资源并处理用户的交互操作,实现页面的完整性和交互性。

在具体实践中,可以从以下几个方面着手:

  1. 前端加载性能:减少首屏加载时间和资源体积,优化用户体验。
  2. 运行时性能:提高页面渲染和交互的流畅性,降低资源占用。
  3. 稳定性和可靠性:确保在高并发或复杂场景下的性能表现一致。
  4. 代码维护性:通过简洁、高效的代码实现功能,降低技术债务和后续开发成本。

无论是哪种优化策略,目标都是在满足实际业务需求的基础上,提供最佳的用户体验和技术可持续性。

实施起来总体就是这几个词:缓存、压缩、懒加载,做好这几个词,基本就做好了大半的性能优化。

接下来我们娓娓道来。

1. 加载时性能优化

我们要让资源加载的快一点,无非就是:资源小一点,少请求一点,当前渲染不要的东西先别加载,请求的网速快一点。我们挨个讲。

资源小一点

怎么让资源可以尽可能的小呢?基本逃离不了4个方法:

  • 删除冗余代码
  • 按需加载(包含懒加载)
  • 细颗粒度的代码分割(其实是利用缓存策略)
  • 开启gzip/br压缩
  • 图片体积优化

接下来我们展开讲一讲

资源小一点:删除冗余代码

冗余代码可能来源于未使用的模块、组件、函数、样式或不再需要的第三方库。以下是常用的方法:

方法一: Tree Shaking

什么是 Tree Shaking?

Tree Shaking 是一种通过静态分析移除未使用代码的技术,通常用来优化前端项目中的 JavaScript 和 CSS 代码。

说人话:静态分析就是依赖esm的语法,知道哪个模块引用了,哪个模块没引用,没引用的就删掉。

其名称来源于“摇树”,模拟将用不到的代码从代码树中“摇掉”,使最终的打包体积更小,加载速度更快。

Tree Shaking 的原理

Tree Shaking 的核心依赖于 ES Module 规范 (import/export),因为它是静态导入,可以在构建时确定哪些代码被使用。
CommonJS (require/module.exports) 是动态导入的,难以在编译时进行静态分析,因此不支持 Tree Shaking。
它是 Dead Code Elimination 的一个子集。它首先标记代码中哪些导入的模块未被使用,然后通过代码压缩器(如 Terser)来移除这些死代码。

前端项目如何做 Tree Shaking?

  1. 使用 ESM 格式的模块:确保代码使用 import/export,避免 require/module.exports。
  2. 配置打包工具支持:不管是vite还是wepack,启用生产模式,结合构建工具和适当的配置就可以开启Tree Shaking对未使用代码进行优化。
  3. 静态导入:避免动态导入或在运行时条件下选择模块,因为这些情况无法被静态分析。
  4. 避免副作用 (Side Effects) :如果模块在导入时有副作用(如修改全局变量),打包工具会保留它。通过 sideEffects 配置声明哪些文件安全删除。

代码示例

文件:math.js

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

这里我们定义了四个函数 add、subtract、multiply、divide,并通过 export 导出。

文件:index.js

// index.js
import { add } from './math.js';

console.log(add(2, 3)); // 输出: 5

在 index.js 中,我们只使用了 add 函数,其他三个函数(subtract、multiply、divide)没有被引用。
经过打包后的输出:
如果启用了 Tree Shaking(以 Webpack 或 Vite 为例),构建工具会分析模块的依赖和引用,移除未使用的代码。

// 打包后代码
const add = (a, b) => a + b;

console.log(add(2, 3)); // 输出: 5

未使用的 subtract、multiply、divide 函数已经被移除。

模块级别的冗余代码删除,tree-shaking比较得心应手,但是更细颗粒度的删除,还需要依赖其他手段。

方法二: 压缩插件优化删除
JavaScript 的压缩和优化

通过工具(如 Terser、esbuild 等),可以删除未被引用的代码片段、简化表达式、内联变量等。举个例子:Webpack 配置(Terser)

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production', // 开启生产模式,默认启用代码压缩
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};
// 原始代码
const unused = () => console.log('I am not used!');
const multiply = (a, b) => a * b;
console.log(multiply(2, 3));

//压缩删除后
console.log(2 * 3);

未使用的 unused 函数被移除,表达式 a * b 被直接计算为结果。

CSS 的压缩和优化

未使用的 CSS 选择器可以通过工具(如 PurgeCSS、UnCSS 等)删除。举个例子:PurgeCSS 配置

PurgeCSS 检查 HTML、JS 中使用的类名,仅保留相关样式。

// 安装 PurgeCSS
npm install @fullhuman/postcss-purgecss --save-dev

postcss.config.js

module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.html', './src/**/*.js'], // 检测的文件路径
      defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [], // 提取类名
    }),
  ],
};

示例:

/* 原始 CSS 文件 */
.button {
  color: red;
}

.unused-class {
  background: yellow;
}

/* 优化后 CSS 文件 */
.button {
  color: red;
}

未使用的 .unused-class 样式被移除。

资源小一点:按需加载

按需加载(On-demand Loading) 是前端优化的一种核心手段,指在应用运行过程中,仅在用户需要时动态加载特定资源(如JavaScript 代码、CSS 文件、组件、图片等),而非一次性加载所有资源。它的核心思想是 「延迟加载非必要内容」,类似于分批处理任务,通过减少初始加载量来提升性能。

简单来说,传统的 Web 页面在加载时,会一次性加载所有的 JavaScript 和 CSS 文件,即使其中的很多资源在当前页面并未使用。而按需加载则可以 分批加载 这些资源,减少不必要的加载,提高网页的加载速度和运行效率。具体体现在:

  • 减少首屏加载时间:避免一次性加载所有资源,加快网页首次渲染速度,提高用户体验。
  • 优化性能:减少浏览器的解析和执行负担,避免加载无用的代码,提高运行效率。
  • 节省带宽:只加载需要的资源,减少不必要的网络请求,降低服务器和用户的流量消耗。
  • 提升交互体验:在用户实际需要时才加载资源,比如滚动到某个部分才加载图片,提高页面的响应速度。

那按需加载的如何实现?按需加载的理论基础是什么?

按需加载其实是基于模块化的理论基础。要不是代码可以分模块写,那就根据需求加载特定模块就无从谈起。

那我们在前端的项目中,一般是依靠 动态导入(Dynamic Import)语法,也就是 import(…)。
这种写法在没有成为规范之前是依赖webpack等打包工具的支持,但是2023年之后也成为了ECMA Script 标准的一部分,百分之九十以上的浏览器都支持。

不使用动态导入的写法:import APage from ‘…/pages/APage’

使用动态导入的写法:const APage = import(‘…/pages/APage’)

使用动态导入语法,那么将会得到一个 promise ,加载成功是 fufiled,加载失败是 rejected。而在构建的时候,对应的模块会被拆分成对应的区块,也就是chunk文件,是独立的。在代码运行时会动态添加script标签,触发加载和执行。

具体如何实施按需加载呢?

在前端项目中,可以通过多种方式实现按需加载,常见的方法包括:

  • 代码层面的动态导入
  • 组件级的懒加载
  • 图片和静态资源的懒加载
  • 路由级的懒加载
  • 服务端返回数据的按需加载
代码按需加载
  • 通过 JavaScript 动态导入(import()) ,在代码执行到某个逻辑时再加载对应的模块,而不是在页面加载时就全部加载。
  • 主要应用于 大型前端项目,避免一次性加载全部 JavaScript 代码。

示例(使用 Webpack/Vite 实现代码分割):

// 传统方式(非按需加载)
import HeavyComponent from './HeavyComponent';

// 按需加载方式(动态导入)
const loadComponent = async () => {
  const { default: HeavyComponent } = await import('./HeavyComponent');
  return HeavyComponent;
};

⚡ 适用场景:第三方库按需加载、大型组件加载、页面内嵌功能的动态加载。

组件级的按需加载

在 React、Vue 这些前端框架中,可以使用 lazy 和 Suspense(React)或 defineAsyncComponent(Vue)实现组件的按需加载。

还有第三方组件库(antd)的按需加载(放到后面的vue项目优化中讲)

React 组件按需加载示例:

import React, { lazy, Suspense } from 'react';

// 按需加载组件
const LazyComponent = lazy(() => import('./LazyComponent'));

const App = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </Suspense>
);

⚡ 适用场景:大组件、弹窗、富文本编辑器等复杂组件的按需加载。

Vue 组件按需加载示例(Vue 3):

import { defineAsyncComponent } from 'vue';

export default {
  components: {
    LazyComponent: defineAsyncComponent(() => import('./LazyComponent.vue')),
  },
};

⚡ 适用场景:Vue 组件按需加载,减少初始 bundle 体积。

图片按需加载

对于图片等静态资源,可以使用 懒加载(Lazy Load) 技术,在用户 滚动到图片可见时才加载。

HTML 原生懒加载(现代浏览器支持):

<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="Lazy Image">

JavaScript 手动实现图片懒加载(适用于所有浏览器):

const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // 赋值真实图片路径
      observer.unobserve(img);
    }
  });
});

images.forEach(img => observer.observe(img));

⚡ 适用场景:长列表的图片加载、博客文章、新闻网站等。

路由按需加载

前端路由可以使用 懒加载,在用户访问某个页面时才加载该页面对应的 JS 代码,而不是一次性加载所有页面。

Vue Router 懒加载示例:

const routes = [
  {
    path: '/about',
    component: () => import('./views/About.vue'), // 按需加载
  },
];

React Router 懒加载示例:

import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

⚡ 适用场景:大型单页应用(SPA)优化首屏加载速度。

数据按需加载

在请求接口时,只加载当前需要的数据,减少不必要的请求和数据传输量。

示例(前端分页请求数据):

const loadMoreData = async (page) => {
  const response = await fetch(`/api/data?page=${page}`);
  const data = await response.json();
  renderData(data);
};

⚡ 适用场景:表格分页、无限滚动、搜索结果分页。
那落实到实际我们要怎么去执行上面的方案来达到项目整体的按需加载呢?

  • 分析项目需求,找出哪些资源可以按需加载(JS 代码、组件、图片、路由、数据等)一般可以通过一些插件来分析打包的产物,例如 webpack-bundle-analyzer-plugin ,看看哪些产物比较大且又是初始化的时候不需要加载的,可以单独抽出来按需加载。
  • 使用 Webpack/Vite 代码分割,通过 import() 进行动态加载。
  • 在 React/Vue 中使用懒加载组件,使用 React.lazy() 或 defineAsyncComponent() 。
  • 优化图片加载,使用 loading=“lazy” 或 IntersectionObserver 进行懒加载。
  • 使用路由懒加载,按需加载路由页面,提高首屏加载速度。
  • 合理进行数据加载优化,使用分页请求、按需请求 API,减少不必要的网络请求。
资源小一点:细颗粒度的代码分割

什么是细颗粒度的代码分割方案?

代码分割是利用现代前端构建工具的功能,将原本的单一构建文件拆分成多个小文件,从而提高缓存的命中率,进而优化用户体验。

为什么拆得小了,拆得多了就会提高缓存命中率?

因为我们常见的缓存策略是:html文件不缓存,每次都去请求最新的html文件。静态资源文件是通过构建工具打了hash值的tag的,只要资源文件发生变化,就会生成新hash,从而命中不了缓存,达到获取新资源的目的。(这个原理不懂可以先问下AI)
那由此可知,拆分的更细了,代码文件之间的影响就更小了。例如模块a和模块b不拆分时打包到一个文件ab-chunk中,
那么a或者b模块变了,都要完整再加载一次ab-chunk资源,但是如果模块a和模块b分开了,就互不影响了,能更最大化的命中缓存。

具体的实践方案网上有很多了,这里不展开。

资源小一点:Gzip压缩
什么是Gzip压缩?

Gzip是一种常用的数据压缩格式,它可以通过对HTTP响应内容进行压缩,减小文件的体积,从而加快网页加载速度,提升前端性能。在前端开发中,Gzip压缩主要应用于HTTP响应中传输的文本文件(如HTML、CSS、JavaScript等)。

说点人话:gzip就是一个我们常见的压缩资源的一种方式,你只需要在请求头写上:accept-encoding:gzip 服务器就能知道要开启压缩,从而压缩资源并返回,浏览器接受到压缩资源进行解压。本质就是牺牲服务器的开销和浏览器的开销来换取资源的最小化,从而提升加载速度。

gzip压缩一般能帮我们压缩到原本资源的70%大小,但也不是所有情况下的压缩效率都有这么高。gzip压缩背后的原理是,在一个资源文件中找重复,并临时替换这些重复,从而就缩小整个文件,所以资源文件中的代码重复率越高,压缩效率也越高。

那问题又来了,用服务器的压缩时间和浏览器的解压时间来换取资源缩小,真的值得吗?

是的这个问题也不能说绝对,但是对于绝大部分情况来说,都是值得的,压缩和解压的时间相对于资源缩小而带来的传输速度的提升是微不足道的,除非你的文件超级小1k、2k。

那作为前端开发人员,如何利用Gzip呢?

webpack Gzip + 服务器 Gzip 的最佳实践

使用构建工具开启Gzip压缩:

Webpack 中的 Gzip 压缩操作本质上是在构建阶段提前对静态资源(如 JS、CSS、HTML)进行 Gzip 预压缩,并将压缩后的 .gz 文件作为构建产物输出。这样,在部署时,服务器无需动态压缩这些文件,而是直接提供预压缩的 .gz 文件,减少了服务器的 CPU 负担,提高了响应速度。

在 Webpack 中,我们使用 compression-webpack-plugin 这个插件来实现 Gzip 预压缩。这个插件在 Webpack 打包时,针对符合条件的静态文件(通常是 .js、.css、.html),使用 Gzip 算法生成 .gz 文件,并将其与普通未压缩的文件一起输出到 dist 目录。

Webpack 在打包时执行 compression-webpack-plugin

  • 遍历所有构建生成的文件,筛选出符合压缩条件的文件(如 .js、.css)。
  • 使用 zlib 进行 Gzip 压缩,生成 .gz 文件(如 bundle.js.gz)。
  • 这些 .gz 文件被保留在构建产物中,等待部署。

在 Webpack 配置 compression-webpack-plugin,生成 .gz 版本的 JS、CSS、HTML 资源:

  const CompressionWebpackPlugin = require('compression-webpack-plugin');

  module.exports = {
    plugins: [
      new CompressionWebpackPlugin({
        algorithm: 'gzip',
        test: /.(js|css|html)$/, // 需要压缩的文件类型
        threshold: 10240, // 只有大于 10KB 的文件才进行压缩
        minRatio: 0.8, // 只有压缩比低于 0.8 才会被压缩
        deleteOriginalAssets: false, // 是否删除原始文件,通常保留原始文件,方便回退
      })
    ]
  };

服务器配置

服务器(如 Nginx 或 Apache)需要配置规则,当请求的文件存在 .gz 版本时,直接返回压缩后的文件,并添加 Content-Encoding: gzip 响应头。服务器优先返回 Webpack 预压缩的 .gz 文件

Nginx 配置

  server {
    gzip on;
    gzip_disable "msie6"; # 禁止 IE6 使用 gzip
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    gzip_vary on;
        
    # 优先返回 gzip 版本的文件
    location / {
      try_files $uri $uri.gz $uri/ =404;
      add_header Content-Encoding gzip;
    }
  }

Webpack Gzip 预压缩和服务器 Gzip 各有优势,它们并不是互相替代的,而是可以互相配合,以优化性能:

  • Webpack 预压缩静态资源,减轻服务器 CPU 负担,提高文件加载速度。
  • 服务器 Gzip 处理动态内容,如 API 响应的数据压缩,确保整体性能优化。
  • 正确配置服务器,优先提供 .gz 版本的文件,保证浏览器能够正确加载 Gzip 资源。

这样可以做到资源加载快,服务器压力小,整体性能最佳

资源小一点:图片体积优化

图片的优化对前端性能至关重要,原因有几个方面。首先,图片是现代网页中最常见且最占用带宽的资源之一。在大多数网站中,图片的体积通常占据了整个页面资源的很大一部分,尤其是对于内容丰富的页面(如电商网站、博客、新闻网站等)而言。如果不对图片进行优化,加载这些图片会大大拖慢页面的渲染速度,影响用户体验,甚至增加CDN或服务器的负担。

为什么图片优化对前端性能优化至关重要?

  1. 大大降低页面加载时间: 图片通常是网页上最大且最重的资源之一。未经优化的图片会导致网络请求时间过长,尤其是在带宽有限或移动网络环境下,图片加载可能会成为瓶颈。如果使用压缩或更高效的格式(如WebP、AVIF等),可以显著减少图片文件的大小,进而减少页面加载时间。
  2. 提高缓存命中率: 对图片进行优化后,它们的体积会变小,缓存策略能够更有效地工作。较小的图片资源可以减少浏览器缓存中占用的空间,提高缓存的命中率,从而加快后续访问同一页面时的加载速度。
  3. 降低带宽消耗: 优化图片可以减少传输的数据量,进而减少带宽消耗。对于带宽有限的用户,图片的快速加载和小文件体积是非常重要的,尤其是在移动设备或慢速网络条件下。
  4. 提升用户体验: 网页加载速度直接影响到用户体验。页面加载时间过长可能会导致用户流失。图片优化有助于提升页面的加载速度和响应时间,从而提升用户的浏览体验。

常见的图片类型主要可以分为以下几类:

  1. JPG(JPEG) :
    • 特点:一种有损压缩的图片格式,广泛应用于照片类图片。体积较小,支持多种色彩(适用于复杂色彩的图片,如风景照、人物照等)。
    • 优化方式:通过调整压缩率来减小图片体积。对于需要处理大量照片的场景,使用适当的压缩率可以减小体积,但不牺牲过多的视觉质量。
  2. PNG:
    • 特点:一种无损压缩的格式,支持透明通道,适用于图标、网页元素、截图等。
    • 优化方式:对于没有透明通道的PNG图片,建议使用pngcrush、optipng等工具进行优化,减小无损压缩后的体积。对于透明图片,使用WebP或者AVIF等格式可以替代PNG,进一步降低体积。
  3. GIF:
    • 特点:常用于制作动画图像。支持多帧动画,但颜色深度有限(最多256种颜色)。文件体积较大。
    • 优化方式:尽量避免使用GIF动画,尤其是大尺寸的GIF,可以考虑使用WebP或APNG(Animated PNG)作为替代,提供更好的质量和更小的体积。
  4. SVG:
    • 特点:矢量图格式,适用于简单的图形、图标和标志。可缩放,且不失真。适用于动态、交互式图形。
    • 优化方式:对SVG文件进行清理,去掉多余的元数据、注释和空格,进一步减小文件体积。可以通过在线工具(如SVGO)来优化SVG文件。
  5. WebP:
    • 特点:支持有损压缩和无损压缩,适用于Web。比JPG、PNG等格式有更高的压缩率,体积更小,支持透明通道。
    • 优化方式:直接将图片转换为WebP格式,利用其高效压缩算法来减少文件体积,尤其适用于大批量图片的优化。
  6. AVIF:
    • 特点:一种较新的图片格式,支持高效的有损和无损压缩,支持透明通道,图像质量较高,体积较小。
    • 优化方式:将图片转换为AVIF格式,能够提供更小的体积和更好的图像质量,尤其适用于图像内容较复杂的场景。
      在这里插入图片描述
      通过上表可以看出,图片格式大致可分为两类:
  • 传统图片格式:如JPG、PNG、GIF、SVG等,这些格式出现已有二十多年。
  • 现代图片格式:如WebP、AVIF等,这些格式是近十年内新出现的。

从功能和性能上来看:

  • 体积:传统格式的图片文件普遍较大。与JPG格式相比,WebP格式通常可以将文件体积减少约10%,而AVIF格式甚至能减少超过40%的体积。
  • 特性:现代格式支持更多特性,如动态图片和无损压缩等,而传统格式的特性相对单一。
  • 浏览器兼容性:现代格式的浏览器兼容性稍逊一筹,支持它们的浏览器数量相对较少。

如果能够使用WebP、AVIF等现代图片格式,可以显著解决前端应用中图片文件体积大、加载慢、CDN开销高等问题,从而提升性能。

然而,由于浏览器兼容性问题,我们不敢完全依赖这些现代格式,无法大规模应用。

如何进行图片性能优化?
  • 选择合适的图片格式:根据图片的用途选择合适的格式。例如,照片类图片优先使用JPG或WebP,图标和UI元素则使用SVG或WebP。对于需要透明度的图片,优先使用WebP或AVIF。

  • 使用现代格式:尽可能使用WebP和AVIF等现代格式,这些格式提供更高的压缩比和更小的体积,能够显著提升页面加载速度,尤其是在图片数量较多的页面上。结合 元素去做,例如:

    <picture>
      <source type="image/avif" srcset="https://cdn.com/image.avif" />
      <source type="image/webp" srcset="https://cdn.com/image.webp" />
      <img src="https://cdn.com/image.jpg" alt="Example" />
    </picture>

就是从上到下哪个兼容用哪个。

  • 调整图片质量与尺寸:
    • 在不显著影响视觉效果的前提下,降低图片的质量和尺寸。例如,通过调整JPG的压缩率,或者使用更高效的PNG优化工具,如pngcrush或optipng。
    • 使用合适的图片尺寸:确保图片的尺寸与实际显示需求相匹配,不要上传过大的图片。例如,在响应式设计中,根据不同设备分辨率和屏幕尺寸加载不同大小的图片。
  • 图片懒加载:这个前面有聊过,图片懒加载是延迟加载图片的技术,只有当图片即将进入视口时才会加载,从而减少初始页面加载的资源消耗,提高页面响应速度。
  • 使用CDN:将图片托管到CDN(内容分发网络)上,使得图片能够从离用户最近的服务器加载,减少网络延迟,提高加载速度。各大云服务供应商都提供了图片CDN服务,除了基本的资源存储功能外,还附加了多种强大功能,例如:
    • 格式转换与体积压缩
      • 原始图片URL:cdn.example.com/image.jpg
      • 转换为WebP格式并压缩:cdn.example.com/image.jpg?format=webp&quality=80
      • 作用是将JPG格式转换为WebP,同时降低图片质量至80%,减少体积,提高加载速度。
    • 图片尺寸缩放
      • 原始图片(原尺寸): cdn.example.com/image.jpg
      • 缩放至宽度300px,高度自动适配:cdn.example.com/image.jpg?width=300
    • 添加水印
      • 原始图片:cdn.example.com/image.jpg
      • 添加水印(水印文本“Sample”):cdn.example.com/image.jpg?watermark=text:Sample,opacity:50

这些功能极大地简化了图片处理的流程,前端无需额外编写代码或使用第三方工具,即可按需动态调整图片,优化网站性能。

我们还可以封装一个图片组件,来根据不同入参获取不同质量和兼容性的图片。

  • 自动化优化工具:使用构建工具和图片处理工具自动化优化图片。例如,使用image-webpack-loader、sharp等工具,在Webpack构建过程中自动压缩和转换图片格式。
  • 图像精灵:将多个小图片(如图标)合并成一张大的图片,使用CSS定位来显示不同的部分,减少HTTP请求次数(减少资源加载时间)。

图片优化对于前端性能的提升具有非常重要的意义,合理选择图片格式、进行图片压缩、尺寸调整、懒加载和使用CDN等技术,都能显著提高网站加载速度,改善用户体验。随着Web的不断发展,现代图片格式(如WebP、AVIF)为我们提供了更高效的图片压缩方式,而根据实际需求选择合适的图片格式和优化手段,能使得网站在加载时事半功倍。

少请求一点

少请求一点:缓存

我们前面说过,性能优化无非就是三个词: 缓存 、压缩、 懒加载。这一节我们来讲如何通过 缓存 来优化性能。

在前端性能优化中,缓存是一个非常重要的手段,能够显著提高网页的加载速度,减少服务器请求,减轻网络压力,从而提升用户体验。通过合理使用缓存,我们可以在不同场景下存储数据和资源,避免重复加载和计算,提升响应速度。

什么是缓存?

缓存是指将某些数据存储在一个临时的存储介质(如内存、硬盘或浏览器等)中,以便在以后需要时能够更快速地获取。缓存的目的是避免每次请求都从头开始计算或加载,而是直接从缓存中获取数据或资源,从而提升效率和减少不必要的延迟。

缓存的类型有哪些?

缓存可以分为多种类型,每种缓存都有不同的应用场景和作用。常见的缓存类型包括:

  1. 浏览器缓存(Client-side Cache):浏览器缓存是在用户浏览器中存储资源(如图片、CSS、JS文件等),以便在下一次访问同一页面时无需重新下载这些资源。主要通过HTTP头部控制,如 Cache-Control、ETag、Last-Modified 等。
  2. DNS缓存
    • DNS缓存是指在本地设备(如浏览器、操作系统、路由器等)中缓存DNS解析结果的机制。当你访问一个网站时,浏览器需要通过DNS(域名系统)将域名(如www.example.com)转换为对应的IP地址。这个过程称为DNS解析。
    • 在首次访问某个域名时,DNS解析器会向域名的DNS服务器发起请求来获取域名的IP地址。为了避免每次都需要重新解析相同的域名,DNS结果会被缓存一段时间,这段时间被称为TTL(Time To Live)。TTL过期之前,设备会直接使用缓存的IP地址,而不必再次进行DNS解析。
  3. HTTP缓存(Server-side Cache):HTTP缓存是指在服务器与客户端之间通过HTTP协议进行的缓存处理。服务器会将请求的数据缓存下来,若下次相同请求到来,直接返回缓存的内容,而不再进行计算或查询。常见的缓存策略有:客户端缓存、代理缓存(如CDN缓存)、服务器缓存等。
  4. CDN缓存(Content Delivery Network Cache):CDN缓存是通过将静态资源(如图片、JS、CSS等)分发到全球各地的CDN节点,减少用户请求的响应时间。CDN缓存提高了静态资源加载的速度,并减轻了源服务器的压力。
  5. 内存缓存(In-memory Cache)
    内存缓存是将常用数据存储在内存中,减少磁盘I/O操作,提高访问速度。常见的内存缓存技术有:Redis、Memcached。
  6. 本地存储(Local Storage / Session Storage)
    • LocalStorage:浏览器提供的一种持久化存储方式,数据不会过期,适合存储不频繁变化的数据,如用户信息、设置等。
    • SessionStorage:与LocalStorage类似,但数据仅在当前会话期间有效。适用于存储会话级别的临时数据。
  7. Service Worker缓存
    • Service Worker是一个可以在后台线程运行的JavaScript,它能够拦截网络请求并将响应存储到缓存中。
    • Service Worker缓存主要用于支持离线功能,使得应用能够在没有网络的情况下继续运行。
如何通过缓存来提升性能?

缓存优化可以有效减少重复加载和计算,提升页面的加载速度和响应能力。我们接着上面的分类挨个讲:

浏览器缓存

在浏览器缓存机制中,强缓存和协商缓存是两个核心概念,它们帮助浏览器决定资源是否需要重新请求服务器,进而优化页面加载速度。两者有不同的工作原理和应用场景。下面,我将详细解释这两种缓存的内容,以及它们是如何变化的。
- 强缓存(Cache-Control、Expires)
- 强缓存是一种最常见的缓存方式,它会直接判断资源是否在缓存有效期内,如果有效,就会直接从缓存中加载资源,而不需要与服务器进行任何交互。
- 强缓存通过设置 Cache-Control 或 Expires 来控制资源的缓存策略。它的关键点是:在缓存的有效期内,浏览器会直接使用缓存中的资源,而不会向服务器发起任何请求。
- Cache-Control: 这是一个现代的、灵活的缓存控制头部,可以用来指定资源的缓存策略。例如:
- max-age: 指定资源在缓存中的最大存活时间(单位为秒)。比如,Cache-Control: max-age=3600 表示资源会被缓存1小时。
- public: 表示资源可以被任何缓存服务器缓存(即使是代理服务器也可以缓存)。
- private: 表示资源只能被用户浏览器缓存,不能被代理服务器缓存。
- no-cache 和 no-store: 强制不缓存资源,其中 no-store 是最严格的缓存控制,表示不允许缓存。
- Expires: 这是一个过时的缓存头部,用来指定资源的过期时间。比如 Expires: Wed, 21 Oct 2025 07:28:00 GMT。不过,Expires 已经被 Cache-Control 替代,Cache-Control 提供了更多的控制选项。

  • 强缓存的变化过程
    - 首次请求:浏览器请求资源时,服务器会返回带有缓存控制头部的响应,浏览器根据 Cache-Control 或 Expires 判断是否缓存资源。
    - 有效缓存:如果缓存仍然有效,浏览器就会直接从缓存中加载资源,而不会向服务器发起请求。
    - 过期缓存:如果缓存已经过期,浏览器会再次向服务器发送请求,获取最新的资源。举例:
    - Cache-Control: max-age=3600:资源缓存1小时。在这1小时内,浏览器不会发起任何请求,直接使用缓存。如果超过1小时,缓存失效,浏览器会重新请求资源。
    - Cache-Control: no-cache:表示每次都需要与服务器确认缓存是否有效,即使资源存在缓存,浏览器也会发送请求进行验证。
    • 协商缓存(ETag、Last-Modified)
      • 协商缓存是一种浏览器与服务器之间的缓存机制,它依赖于浏览器和服务器的通信来确定资源是否有变化。如果资源没有变化,服务器会返回 304(Not Modified)响应,告诉浏览器继续使用缓存中的资源。
      • 协商缓存的核心是 ETag 和 Last-Modified 这两个HTTP头部。它们用于帮助浏览器和服务器之间判断资源是否发生变化,

具体步骤如下:
- ETag:服务器会在响应头中返回一个唯一标识符(如哈希值),该标识符代表资源的内容。如果资源内容没有发生变化,ETag 就不会改变。
- Last-Modified:服务器会在响应头中返回资源最后修改的时间。如果资源没有被修改,服务器返回的时间就不会变化。

当浏览器使用缓存的资源时,会在请求头中带上这些标识符,服务器会根据这些标识符判断资源是否有变化:
- If-None-Match:浏览器在请求时带上上次请求的 ETag 值,服务器比较 ETag 是否相同,如果相同则返回 304(Not Modified)。
- If-Modified-Since:浏览器会发送 Last-Modified 值,服务器检查文件自上次修改以来是否被更新,如果未更新,则返回 304。

协商缓存的变化过程:
- 首次请求:服务器返回资源并附带 ETag 或 Last-Modified,浏览器将其存储起来。
- 后续请求:浏览器向服务器发送带有 If-None-Match 或 If-Modified-Since 的请求头,询问服务器资源是否有变化。
- 服务器响应:如果资源没有变化,服务器返回 304 状态码,浏览器继续使用本地缓存。如果资源变化,服务器返回新的资源和新的 ETag 或 Last-Modified。

举例:
- ETag:
- 服务器返回:ETag: “12345”
- 浏览器请求时发送:If-None-Match: “12345”
- 如果资源未修改,服务器返回 304 状态,告诉浏览器继续使用缓存。
- Last-Modified:
- 服务器返回:Last-Modified: Tue, 20 Apr 2021 12:00:00 GMT
- 浏览器请求时发送:If-Modified-Since: Tue, 20 Apr 2021 12:00:00 GMT
- 如果资源未修改,服务器返回 304 状态。

强缓存:通过设置缓存过期时间(Cache-Control 或 Expires)来控制资源的存储期,缓存有效期内直接使用缓存,不与服务器交互。适用于静态且更新不频繁的资源。

协商缓存:依赖 ETag 和 Last-Modified,通过与服务器的通信来验证缓存是否有效。适用于需要频繁更新且服务器内容不可预知的资源。

这两种缓存方式可以组合使用,先使用强缓存,如果强缓存失效,再通过协商缓存向服务器验证资源的有效性,从而提供最优的缓存策略。
在这里插入图片描述
举例:对于图片、JS、CSS等静态资源,可以设置较长的缓存过期时间,而对于频繁更新的内容(如动态HTML页面),可以设置较短的缓存时间或不缓存。

谷歌给了一张图,更好的说明了应该怎么去指定资源缓存的策略

在这里插入图片描述

总结一下上面的图。首先,如果资源内容完全不可复用,那就直接把 Cache-Control 设置为 no-store,拒绝任何形式的缓存。否则,问自己一个问题:每次都要去服务器确认缓存是否有效吗?如果是,那就设为 no-cache。接下来,思考下这个资源是否可以被代理服务器缓存,基于这个决定,将其设置为 private 还是 public。然后,进一步考虑资源的过期时间,合理配置 max-age 和 s-maxage。最后,别忘了配置协商缓存所需要的 ETag、Last-Modified 等参数。

DNS缓存

DNS缓存是指在本地设备(如浏览器、操作系统、路由器等)中缓存DNS解析结果的机制。当你访问一个网站时,浏览器需要通过DNS(域名系统)将域名(如www.example.com)转换为对应的IP地址。这个过程称为DNS解析。

在首次访问某个域名时,DNS解析器会向域名的DNS服务器发起请求来获取域名的IP地址。为了避免每次都需要重新解析相同的域名,DNS结果会被缓存一段时间,这段时间被称为TTL(Time To Live)。TTL过期之前,设备会直接使用缓存的IP地址,而不必再次进行DNS解析。

DNS缓存的工作原理:

  1. 首次请求:浏览器向DNS服务器发起查询请求,服务器返回域名对应的IP地址,并设置TTL。
  2. 缓存存储:浏览器将返回的IP地址存储在DNS缓存中,并在TTL有效期内使用该IP地址。
  3. 过期查询:TTL到期后,浏览器会再次发起DNS查询,获取最新的IP地址。

DNS缓存不仅存在于浏览器中,还可以在操作系统和网络设备(如路由器)中缓存DNS结果。

那DNS缓存如何提升性能?

DNS解析的过程是必须的,但每次请求都进行DNS解析会增加额外的时间延迟。利用DNS缓存可以避免重复的解析请求,从而减少加载时间,提升网站的访问速度。

具体来说,利用DNS缓存可以带来以下优化:

  • 减少DNS查询延迟:当域名的IP地址已经在缓存中,浏览器可以直接读取缓存,避免再次向DNS服务器发起请求。
    如果缓存命中,DNS解析就可以在几毫秒内完成,而如果不命中,则需要通过网络查询DNS服务器,耗时会较长。
  • 减少网络请求:通过避免重复的DNS查询,减少了客户端与DNS服务器之间的通信,减少了网络延迟。特别是在用户访问同一网站的多个页面时,DNS缓存可以显著降低每次页面加载的时间。
  • 加速多域名资源加载:如果一个页面需要加载来自多个不同域名的资源(例如,CDN上的图片、JavaScript文件、API请求等),DNS缓存可以避免重复解析这些域名,从而加速资源加载。

作为前端开发人员,虽然我们不能直接控制DNS解析过程,但可以采取一些措施,利用DNS缓存来优化性能:

  • 使用持久的域名和合理的TTL设置
    • 选择稳定的域名:选择长期存在的、稳定的域名,避免频繁更换域名。因为每次更换域名都会导致DNS缓存失效。
    • 合理配置TTL值:对于CDN、静态资源等不频繁变化的资源,可以配置较长的TTL(如几小时或几天),这样浏览器在一段时间内会缓存DNS解析结果,避免频繁解析。对于动态内容,可以设置较短的TTL,确保DNS解析结果保持最新。
  • 减少跨域DNS查询
  • 避免过多的第三方域名:尽量减少页面中跨多个域名的请求(比如加载不同域名下的图片、字体、广告等)。每个新的域名都需要进行一次DNS解析。如果这些域名都缓存得不好,会增加DNS查询的次数和延迟。
    - 合并请求:尽量将多个静态资源合并到一个域名下,这样浏览器只需要解析一次DNS,减少额外的延迟。
  • 利用DNS预解析(DNS Prefetch)
    • DNS预解析是HTML中的一个技术,可以让浏览器提前解析将要请求的域名。通过标签,浏览器可以提前解析特定域名,从而加快后续请求的响应速度。

举例:如果你知道网站会加载外部CDN资源,可以在页面中提前指定DNS预解析。

     <link rel="dns-prefetch" href="https://cdn.example.com">

这样,在浏览器加载页面时,会提前解析cdn.example.com的IP地址,减少后续对该域名的DNS解析时间。

使用HTTP/2(多路复用)

HTTP/2协议允许多个请求共享一个TCP连接并进行多路复用,减少因多个域名引发的DNS查询延迟。通过使用HTTP/2,可以提高多个域名下资源加载的并行性,并且通过较少的TCP连接减少DNS解析的次数。
例如,当你使用多个域名加载资源时,浏览器必须分别为每个域名进行DNS解析。HTTP/2可以优化这一点,降低域名解析和建立连接的时间。

合理使用CDN

CDN缓存:使用CDN可以将静态资源分发到离用户更近的服务器,并且CDN会缓存DNS解析结果。这样,当用户请求相同资源时,不仅可以加速资源加载,DNS解析也会被缓存,减少了DNS查询的次数和延迟。
设置和管理子域名
- 使用子域名:如果有多个独立的子系统或服务,可以通过子域名来拆分资源,缓存不同域名的DNS结果。比如,你可以为静态资源、API服务、图片等分别设置不同的子域名,如 assets.example.com、api.example.com、images.example.com 等。
- 避免频繁修改DNS记录:如果频繁更换子域名的DNS记录会导致DNS缓存被清空,从而增加解析延迟。因此,要谨慎管理DNS记录和TTL设置。

通过合理利用DNS缓存,我们可以有效减少DNS解析时间,提高页面加载速度,提升用户体验。具体优化方法包括:

  • 选择长期稳定的域名,并合理设置TTL值;
  • 减少跨域DNS查询,通过合并资源减少DNS解析次数;
  • 利用DNS预解析加速外部资源加载;
  • 使用HTTP/2协议优化多个资源加载;
  • 使用CDN加速静态资源加载,同时缓存DNS结果。

通过这些方式,前端开发人员能够有效利用DNS缓存提升网站的性能,减少延迟,提高用户体验。

CDN缓存

利用CDN缓存加速资源加载

CDN通过将资源缓存到离用户更近的节点,使得静态资源可以从最近的服务器获取,从而加快加载速度并减轻源服务器的负担。使用CDN缓存可以显著提高网站的响应速度,尤其是对于跨地域的访问。

举例:对于全球访问的网站,部署CDN缓存,可以确保资源的快速加载,避免重复请求源服务器。

Service Worker缓存

实现离线缓存

使用Service Worker可以缓存页面的资源,甚至实现离线访问功能。当用户没有网络连接时,Service Worker可以从缓存中获取资源,使得应用仍然可以正常显示。

举例:通过Service Worker缓存首页、图标、样式文件等资源,确保即使在没有网络的情况下,用户也能访问应用的一部分内容。
示例代码:

 self.addEventListener('install', event => {
   event.waitUntil(
     caches.open('my-cache').then(cache => {
       return cache.addAll([
         '/index.html',
         '/style.css',
         '/app.js',
         '/logo.png'
       ]);
     })
   );
 });

 self.addEventListener('fetch', event => {
   event.respondWith(
     caches.match(event.request).then(response => {
       return response || fetch(event.request);
     })
   );
 });
本地存储缓存

利用LocalStorage和SessionStorage缓存数据

使用浏览器的localStorage或sessionStorage来存储页面中的非敏感数据,如用户设置、浏览历史等,可以避免每次加载页面时重新请求相同的数据,减少了请求时间。

举例:存储用户的主题色、登录状态、购物车数据等,以便在页面刷新后恢复。

 // 保存数据
 localStorage.setItem('theme', 'dark');

 // 获取数据
 const theme = localStorage.getItem('theme');
内存缓存(In-memory Cache)

使用内存缓存提高访问速度

内存缓存(如Redis、Memcached)存储的是内存中的数据,访问速度极快。通常用于缓存数据库查询结果、API响应等高频次请求的数据,避免重复计算或重复查询数据库。

举例:缓存API请求的响应,避免每次都查询数据库。例如,一个产品详情页的API可以缓存该页面的响应结果,在一定时间内返回缓存数据,而不是每次都访问数据库。

利用版本控制和哈希

版本控制资源和缓存失效

通过为静态资源文件(如JS、CSS、图片等)添加版本号或哈希值,可以确保文件更新时,浏览器能自动检测并加载新版本资源,而不被缓存旧版本。

举例:当资源文件更改时,通过修改文件名(如app.123456.js)来确保浏览器加载最新版本的文件,而不是使用缓存的旧文件。

总之缓存是前端性能优化中的核心技术之一,能够显著减少网络请求,提升加载速度,减少带宽消耗,最终优化用户体验。通过合理使用浏览器缓存、CDN缓存、内存缓存、本地存储、Service Worker缓存等技术,我们可以大幅提高网页的性能,确保网站在不同场景下的快速响应。

请求快一点

请求快一点:CDN的优化

我们前面的缓存一节中,已经简单介绍过cdn缓存,这一节我们来聊一聊,如何对cdn进行优化从而使得请求得快一点。

什么是cdn?

简单说就是把一些静态资源放到不同地理位置的服务器中,用户访问的时候,就访问最近的那个服务器的资源,速度就会快很多。

那什么是静态资源?

就是如js、css、html 、图片等不需要服务器即时计算的资源就是计算资源。举个例子,如果你请求一个接口,该接口返回最新的数据如:位置信息,实时的画面等,这种就不是静态资源。

那我们应该实施什么行为来优化cdn呢?

  • 根据用户的地理位置分布选择cdn的地理位置
    • 大多数的cdn云服务都有负载服务器遍布大部分地区,但是需要手动切换,所以在选择cdn服务的时候,要看用户的地理位置分布,基于用户来选择cdn服务器。
  • cdn服务器域名和业务域名不同的好与不好
    • cdn服务器的域名和业务域名不一样会导致跨域的问题,对于开发人员来说,解决跨域问题是一件比较繁琐的事情,但是对用户没有直接的不良体验。但由此产生的好处也是大大的。相同的域名下,请求静态资源都会携带cookie,不同的域名反而不需要,否则请求每个静态资源都要携带cookie这对带宽也是一种浪费。
  • 开启压缩算法
    • cdn服务器也是一个标准服务器,那我们自然也要开启压缩,例如 gzip br等压缩功能,能节省资源体积,提升加载速率。
  • 使用最新版本的http协议
请求快一点: http2

HTTP协议经历了多次迭代,每个版本都在前一个版本的基础上进行了优化,解决了性能瓶颈、提高了用户体验,并适应了互联网不断发展的需求。下面是HTTP各个版本的详细发展过程和其特性:

  1. HTTP/0.9 (1991年) — 初代协议
    • 核心特性:
      • 请求方式:只有GET方法,用于请求文本资源(HTML)。
      • 响应格式:只支持纯文本(没有MIME类型)。
      • 没有请求头:没有请求头和响应头,所有的请求和响应都是简单的文本内容。
      • 简单连接:每个请求和响应结束后,都会关闭连接。
    • 适用场景:用于最初的网页浏览,功能非常简单,只适用于文本文件。
  2. HTTP/1.0 (1996年) — 标准化初步完善
    • 核心特性:
      • 请求方式:除了GET,还支持POST方法,可以发送数据。
      • MIME类型:支持传输多种格式的数据(如HTML、图片、音频、视频等),并在响应头中标明数据类型(如 Content-Type)。
      • 请求头和响应头:引入了请求头和响应头,可以携带更多的信息,如用户代理信息、服务器信息、缓存控制等。
      • 短连接:每次请求都需要建立新的TCP连接,完成数据传输后关闭连接。
    • 问题:尽管引入了更丰富的功能,但由于每个请求都需要单独建立TCP连接,造成了大量的开销。

无法复用连接是一个1.0版本比较大的问题

HTTP1.0为每个请求单独新开一个TCP连接
在这里插入图片描述
由于每个请求都是独立的连接,因此会带来下面的问题:

  1. 连接的建立和销毁都会占用服务器和客户端的资源,造成内存资源的浪费
  2. 连接的建立和销毁都会消耗时间,造成响应时间的浪费
  3. 无法充分利用带宽,造成带宽资源的浪费

TCP协议的特点是「慢启动」,即一开始传输的数据量少,一段时间之后达到传输的峰值。而上面这种做法,会导致大量的请求在TCP达到传输峰值前就被销毁了

为了解决1.0的问题于是1.1版本出现了

  1. HTTP/1.1 (1997年) — 大规模应用

HTTP/1.1是最广泛使用的版本,进行了大量改进:
- 核心特性:
- 持久连接(Keep-Alive) :默认开启长连接,可以复用TCP连接进行多个请求和响应,避免了频繁建立和关闭TCP连接的开销。
- 管道化(Pipelining) :客户端可以在等待响应的同时,发送多个请求,服务器按顺序响应这些请求(尽管实际上,很多浏览器直到较晚才支持这一功能)。
- 分块传输(Chunked Transfer Encoding) :允许不确定文件大小的数据分块传输,支持实时传输内容(例如流媒体)。
- 缓存控制:新增了更强大的缓存机制,支持如 Cache-Control、Etag、Last-Modified 等头部,有效提高了资源的复用性。
- 虚拟主机:HTTP/1.1支持通过Host头部区分不同的虚拟主机,实现了同一IP地址下多个网站的托管。
- 更多的请求方法:除了GET和POST,还支持了如PUT、DELETE等HTTP方法,丰富了资源的操作方式。
- 问题:
- 队头阻塞(Head-of-Line Blocking) :尽管支持管道化,但多个请求仍然会在同一连接中按顺序处理。如果某个请求延迟,后续的请求也会受到影响。
- 多个TCP连接:虽然开启了长连接,但由于TCP连接的并发限制,浏览器仍然会为同一个域名创建多个连接,导致开销。

1.1版本最主要是改进了默认开启长链接为了解决HTTP1.0的问题,HTTP1.1默认开启长连接,即让同一个TCP连接服务于多个请求-响应。
在这里插入图片描述
在这种情况下,多次请求响应可以共享同一个TCP连接,这不仅减少了TCP的握手和挥手时间,同时可以充分利用TCP「慢启动」的特点,有效的利用带宽。

实际上,在HTTP1.0后期,虽然没有官方标准,但开发者们慢慢形成了一个共识:

只要请求头中包含Connection:keep-alive,就表示客户端希望开启长连接,希望服务器响应后不要关闭TCP连接。如果服务器认可这一行为,即可保持TCP连接。

当需要的时候,任何一方都可以关闭TCP连接

扩展知识
连接关闭的情况主要有三种:

  1. 客户端在某一次请求中设置了Connection:close,服务器收到此请求后,响应结束立即关闭TCP
  2. 在没有请求时,客户端会不断对服务器进行心跳检测(一般每隔1秒)。一旦心跳检测停止,服务器立即关闭TCP
  3. 当客户端长时间没有新的请求到达服务器,服务器会主动关闭TCP。运维人员可以设置该时间。

由于一个TCP连接可以承载多次请求响应,并在一段时间内不会断开,因此这种连接称之为长连接。

管道化和队头阻塞

HTTP1.1允许在响应到达之前发送下一个请求,这样可以大幅缩减带宽限制时间

但这样做会存在队头阻塞的问题

在这里插入图片描述
由于多个请求使用的是同一个TCP连接,服务器必须按照请求到达的顺序进行响应

于是,导致了一些后发出的请求,无法在处理完成后响应,产生了等待的时间,而这段时间的带宽可能是空闲的,这就造成了带宽的浪费

队头阻塞虽然发生在服务器,但这个问题的根源是客户端无法知晓服务器的响应是针对哪个请求的。

正是由于存在队头阻塞,我们常常使用下面的手段进行优化:
- 通过减少文件数量,从而减少队头阻塞的几率
- 通过开辟多个TCP连接,实现真正的、有缺陷的并行传输

  • 浏览器会根据情况,为打开的页面自动开启TCP连接,对于同一个域名的连接最多6个
  • 如果要突破这个限制,就需要把资源放到不同的域中

然而,管道化并非一个成功的模型,它带来的队头阻塞造成非常多的问题,所以现代浏览器默认是关闭这种模式的
在这里插入图片描述

  1. HTTP/2 (2015年) — 性能革命
    HTTP/2是对HTTP协议的根本性改进,旨在解决HTTP/1.x中的性能瓶颈,尤其是资源加载的延迟问题。其核心优势在于支持多路复用、头部压缩等技术,大大提高了性能。

    • 核心特性:
      • 二进制协议:HTTP/2采用二进制帧(而不是HTTP/1.x的文本协议),提高了数据的传输效率,减少了解析开销。
      • 多路复用(Multiplexing) :通过在一个TCP连接上同时并发多个请求和响应,避免了HTTP/1.x中的队头阻塞问题。多个请求可以同时在同一连接中传输,不再按照顺序依次等待。
      • 头部压缩(HPACK) :HTTP/2对请求和响应的头部进行压缩,减少了冗余信息,提高了带宽利用率。
        服务器推送(Server Push) :服务器可以主动推送资源给客户端,无需等待客户端请求。这有助于提前加载所需的资源(如CSS、JS文件)。
      • 优先级控制:允许客户端指定不同资源的优先级,帮助服务器在有限的带宽下优先传输重要资源。
      • 减少连接数:避免了HTTP/1.x中同一域名下创建多个TCP连接的情况,减少了连接数,从而减少了TCP握手和关闭连接的延迟。
    • 性能提升:通过多路复用和头部压缩,HTTP/2显著降低了延迟,提高了页面加载速度。
    • 局限性:HTTP/2仍然基于TCP协议,存在TCP队头阻塞问题(如果TCP连接出现问题,所有的请求都会受到影响)。另外,由于HTTP/2是二进制协议,某些代理和中间件可能需要更新以支持该协议。

二进制分帧

HTTP2.0可以允许以更小的单元传输数据,每个传输单元称之为帧,而每一个请求或响应的完整数据称之为流,每个流有自己的编号,每个帧会记录所属的流。
在这里插入图片描述
比如,服务器连续接到了客户端的两个请求,一个请求JS、一个请求CSS,两个文件如下:

function a(){}

function b(){}

.container{}

.list{}

最终形成的帧可能如下
在这里插入图片描述
可以看出,每个帧都带了一个头部,记录了流的ID,这样做就能够准确的知道这一帧数据是属于哪个流的。
在这里插入图片描述
这样就真正的解决了共享TCP连接时的队头阻塞问题,实现了真正的多路复用

不仅如此,由于传输时是以帧为单元传输的,无论是响应还是请求,都可以实现并发处理,即不同的传输可以交替进行。

由于进行了分帧,还可以设置传输优先级。

头部压缩

HTTP2.0之前,所有的消息头都是以字符的形式完整传输的

可实际上,大部分头部信息都有很多的重复

为了解决这一问题,HTTP2.0使用头部压缩来减少消息头的体积
在这里插入图片描述

对于两张表都没有的头部,则使用Huffman编码压缩后进行传输,同时添加到动态表中

服务器推送

HTTP2.0允许在客户端没有主动请求的情况下,服务器预先把资源推送给客户端

当客户端后续需要请求该资源时,则自动从之前推送的资源中寻找

  1. HTTP/3 (2022年正式标准化) — 未来趋势

HTTP/3是HTTP协议的最新版本,基于Google的QUIC协议(Quick UDP Internet Connections)。QUIC协议最初是为了提高移动网络下的传输性能而设计的,HTTP/3将QUIC引入到HTTP协议中,彻底改变了数据传输的方式

  • 核心特性:
    - 基于QUIC协议:HTTP/3不再依赖TCP协议,而是使用QUIC(基于UDP),解决了TCP中的队头阻塞问题。
    - 快速连接建立:QUIC协议的最大优势是零往返时间连接建立(0-RTT) ,意味着连接建立几乎是瞬时的,从而减少了延迟。
    - 内置TLS加密:HTTP/3强制使用TLS 1.3加密,提升了安全性,减少了握手延迟。
    - 无队头阻塞:QUIC协议解决了TCP的队头阻塞问题,使得HTTP/3能够同时处理多个请求和响应,即使其中一个请求出现延迟,其他请求也不会受到影响。
    - 移动网络优化:QUIC对于网络切换(如Wi-Fi到4G)的支持更加友好,可以保持连接不中断。
    • 性能提升:HTTP/3通过减少连接建立时间和无队头阻塞大大降低了延迟,尤其在不稳定的网络环境下表现尤为出色。
    • 挑战与前景:
      • 兼容性问题:HTTP/3需要支持QUIC协议的服务器和客户端,目前大部分现代浏览器(如Chrome、Firefox、Edge)已经支持,但某些旧版本仍不兼容。
      • 网络设备更新:由于QUIC使用UDP协议,因此需要网络设备(如防火墙、代理服务器)支持QUIC,可能会带来一些部署上的挑战。

总之

  1. HTTP/0.9:最早的单一文本传输协议。
  2. HTTP/1.0:引入了请求头、响应头和多种数据类型,但仍然是短连接,性能较低。
  3. HTTP/1.1:广泛采用的版本,引入持久连接、管道化、缓存机制等功能,极大改善了性能。
  4. HTTP/2:通过二进制协议、多路复用、头部压缩等方式,解决了性能瓶颈,显著提高了数据传输效率。
  5. HTTP/3:基于QUIC协议,彻底解决了TCP的队头阻塞问题,提升了移动网络下的性能,并提供了更快的连接建立和更高的安全性。

讲完了http的发展,我们现阶段投入产出比最高的优化其实是使用http2.0,这个给我们带来的提升不是一星半点的,现在不管是阿里云还是腾讯云都支持2.0版本了,需要后端和运维同学去配合升级。

请求快一点: 预加载和预链接

资源优先级提示(Resource Priority Hints)是一组浏览器提供的优化手段,可以帮助开发者更精确地控制资源的加载顺序和时机,以减少关键资源的阻塞时间,提升页面的加载速度和用户体验。这些机制包括 Prefetch、Preload、Preconnect、DNS-Prefetch,它们各自针对不同类型的资源加载场景进行优化。下面我们详细讲解每个 API 的概念、用法和最佳实践。

  1. 预取回(Prefetch)
    概念:
    • Prefetch 适用于 即将需要但当前页面不急需 的资源(如下一页的脚本、样式表、图片等)。
    • 浏览器会 在空闲时间 低优先级下载这些资源并缓存,以便用户稍后访问时可以更快加载。

用法:

<link rel="prefetch" href="next-page.js" as="script">
<link rel="prefetch" href="next-page.css" as="style">

适用场景:
- 用户点击某个链接后,下一页的 JS、CSS 已经被预加载,访问下一页时会更快。
- SPA(单页应用)可以预取未来可能访问的页面资源。

最佳实践: ✅ 适用于 下一步可能会用到但当前不影响渲染 的资源,例如:

  • 预测用户行为,如新闻网站、电子商务网站的下一页资源预取。
  • 避免滥用:Prefetch 会占用带宽资源,影响当前页面的加载,因此不适合对大量资源进行预取。
  1. 预加载(Preload)
    概念:Preload 用于显式告诉浏览器高优先级加载某个资源(如 JS、CSS、字体、图片等),以便在关键渲染路径上避免阻塞。

与 Prefetch 的区别:
- Prefetch 是低优先级加载(用于未来页面)。
- Preload 是高优先级加载(用于当前页面)。

用法:

<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="styles.css" as="style">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin="anonymous">

适用场景:

  • 关键资源(如 Web 字体、首屏所需 JS 和 CSS)可以提前加载,避免渲染阻塞。
  • 视频文件的预加载,减少白屏时间。

最佳实践: ✅ 适用于 当前页面必须用到的关键资源,例如:

  • Web 字体:
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin="anonymous">
  • 关键 CSS 或 JS:
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="important.js" as="script">

⚠ 注意:

不要滥用:滥用 Preload 可能会影响其他重要资源的加载。
搭配 as 属性:正确指定资源类型(如 as=“script”),否则浏览器可能不会正确处理。

  1. 预连接(Preconnect)

概念:

  • Preconnect 用于提前建立到第三方服务器的连接,包括 DNS 解析、TCP 握手和 TLS 连接,从而减少请求的延迟。
  • 适用于 跨域资源(如 CDN、API、广告、第三方分析工具) 。

用法:

<link rel="preconnect" href="https://cdn.example.com">

如果第三方服务器需要 CORS 访问,建议加上 crossorigin:

<link rel="preconnect" href="https://cdn.example.com" crossorigin>

适用场景:

CDN 资源(图片、CSS、JS等)加载优化。
Google Fonts、第三方 API、分析工具(如 Google Analytics)。
最佳实践: ✅ 适用于 需要跨域加载资源的情况,例如:

CDN 加载的字体文件

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

加载第三方 API

<link rel="preconnect" href="https://api.example.com">

⚠ 注意:

仅在 确实需要的情况下使用,例如重要的域名,数量也不要超过,否则会浪费 TCP 连接。因为会与目标域名保持10秒的链接,会阻碍其他资源加载

  1. DNS 预取(DNS-Prefetch)
    概念:
    其实跟上面的preconnect有重合,大部分情况下用上面那个就好了
  • DNS-Prefetch 仅用于提前解析域名的 DNS 解析,但不会建立完整的连接。
  • 适用于 低优先级 的跨域资源预解析,提升首次请求的速度。

用法:

<link rel="dns-prefetch" href="//cdn.example.com">

适用场景:

适用于 CDN、第三方 API、广告资源、字体资源等,加快 DNS 解析时间。
最佳实践: ✅ 适用于 第三方资源加载但优先级较低的情况,例如:

广告、分析工具、CDN:

<link rel="dns-prefetch" href="//analytics.google.com">

⚠ 注意:

dns-prefetch 仅进行 DNS 解析,并不会预加载资源,如果资源非常重要,建议改用 preconnect。
在这里插入图片描述
最佳实践示例:

1.优化字体加载

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin="anonymous">

2.优化 CDN 资源

<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="//analytics.example.com">

3.优化首屏渲染

<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="important.js" as="script">
  • 合理使用 Preload,可以显著提高关键资源的加载速度,加快首屏渲染。
  • 使用 Preconnect 和 DNS-Prefetch,可以减少第三方资源的加载延迟,优化网络性能。
  • Prefetch 适用于预测未来的资源需求,避免页面跳转时的加载等待。

通过综合使用这些 API,可以有效减少首屏加载时间,提高用户体验,使 Web 应用更加流畅快速。我们还有现成的帮我们添加这些资源提示词的工具。可以根据构建产物,自动生成资源优先级提示代码。这个你们自己找吧,不赘述。

2. 渲染时的性能优化

3. 开发阶段的优化

4. 分析评估

在前端项目中,性能优化不是盲目的,而是基于量化的指标和监控数据来判断 哪些地方需要优化 以及 如何优化。我们可以通过前端性能监控、分析工具和指标数据,找出性能瓶颈,并针对性地进行优化。

1. 什么是前端性能监控量化?

前端性能监控量化 是指使用可量化的指标(如页面加载时间、交互响应速度、资源大小等)来评估网页或应用的性能,并通过监测这些指标的数据趋势,找出可能的性能瓶颈。

常见的性能优化需要关注以下几方面:

  • 页面加载性能(首屏渲染时间、白屏时间、资源加载速度)
  • 交互响应性能(用户操作的延迟、动画流畅度)
  • 代码执行性能(JavaScript 运行速度、计算密集型任务优化)
  • 网络请求优化(HTTP 请求数、请求大小、CDN 加速)
  • 错误和异常监控(JS 报错、网络请求失败)

2. 前端性能监控的核心指标

前端性能监控指标可以分为 页面加载性能指标 和 交互体验性能指标。

2.1 页面加载性能指标

在这里插入图片描述
🔹 示例:使用 Performance API 监控 LCP

const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  console.log('LCP:', entries[0].startTime);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
2.2 交互体验性能指标

在这里插入图片描述

3. 如何进行前端性能监测?

我们可以通过以下方式对前端项目进行监控:

3.1 使用 Chrome DevTools

Chrome DevTools 提供了一整套分析工具:

  1. Network 面板:查看网络请求、资源加载情况。
  2. Performance 面板:分析 CPU、JavaScript 执行、帧率等。
  3. Coverage 面板:检测未使用的 CSS 和 JavaScript 代码。

🔹 示例:使用 Performance 进行分析

  1. 打开 DevTools(F12 或 Cmd + Option + I)。
  2. 选择 Performance 选项卡。
  3. 点击 Start Profiling and Reload Page 进行录制。
  4. 查看 CPU、网络请求、渲染时间等数据。
3.2 使用 Lighthouse 进行性能评分

Lighthouse 是 Google 提供的开源工具,能够分析前端性能、SEO、可访问性等。
🔹 如何使用 Lighthouse

  1. 在 Chrome DevTools 中运行
    1. 打开 DevTools (F12)
    2. 进入 Lighthouse 选项卡
    3. 点击 “Generate report”
    4. 查看 Performance 分数和优化建议
  2. 使用 CLI 运行
npx lighthouse https://example.com --view
  1. 使用 PageSpeed Insights
    1. 访问 PageSpeed Insights
    2. 输入网站 URL,分析性能
3.3 使用 Web Vitals 监测

Google 提供的 Web Vitals 可以帮助监测 LCP、FID、CLS 等关键指标。

🔹 示例:集成 Web Vitals 进行监控

import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);
3.4 使用 Performance API

Performance API 允许我们在 JavaScript 代码中监控关键性能数据。

🔹 示例:获取页面加载时间

window.addEventListener('load', () => {
  const { loadEventEnd, navigationStart } = performance.timing;
  console.log('页面加载时间:', loadEventEnd - navigationStart, 'ms');
});
3.5 使用第三方监控平台

为了监控线上环境的性能,可以使用以下服务:

在这里插入图片描述
🔹 示例:使用 Sentry 监控前端错误

import * as Sentry from '@sentry/browser';

Sentry.init({
  dsn: 'https://your-dsn@sentry.io/your-project-id',
});

try {
  throw new Error('测试异常');
} catch (error) {
  Sentry.captureException(error);
}

4. 总结

如何找到性能问题?

  1. 使用 Chrome DevTools 分析页面加载情况
  2. 使用 Lighthouse 进行自动化评分
  3. 使用 Performance API 监测关键指标
  4. 使用 Web Vitals 监测 LCP、FID、CLS
  5. 使用第三方监控工具(Sentry、New Relic)

通过这些方法,我们可以准确定位性能瓶颈,并进行针对性优化

5.团队管理和规范制定

团队中要建立文档宣贯以及代码审查,明确我们要怎么写代码才能运行得更好。

6. 常见的方案

6.1 骨架屏

6.2 虚拟列表

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

画一个圆_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值