React 性能优化思路

我觉得React 性能优化的理念的主要方向就是这两个:

  1. 减少重新 render 的次数。因为在 React 里最重(花时间最长)的一块就是 reconciliation(简单的可以理解为 diff),如果不 render,就不会 reconciliation。
  2. 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用。

在使用类组件的时候,使用的 React 优化 API 主要是:shouldComponentUpdatePureComponent,这两个 API 所提供的解决思路都是为了减少重新 render 的次数,主要是减少父组件更新而子组件也更新的情况。

但是在函数式组件里面没有声明周期也没有类,那如何来做性能优化呢?

先分个类,组件什么时候会重新执行?

  1. 组件自己的状态改变
  2. 父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改变
  3. 父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变

针对第二点,在FC中,可以通过memo减少rerender

1
2
3
4
function Component(props) {
/* 使用 props 渲染 */
}
const MyComponent = React.memo(Component);

通过 React.memo 包裹的组件在 props 不变的情况下,这个被包裹的组件是不会重新渲染的(相当于PureComonent)

默认情况下其只会对 props 的复杂对象做浅层对比(浅层对比就是只会对比前后两次 props 对象引用是否相同,不会对比对象里面的内容是否相同),如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

阅读全文 »

面向对象

类的修饰符

  • public:无访问限制
  • protected:仅当前类中的方法或子类中的方法能访问
  • private: 仅当前类的方法能访问,子类和外部都不行

类的继承

可以使用 extends 复用父类的逻辑和属性

抽象类

  • abstract关键字修饰的类,不能直接实例化,只能被子类继承;
  • 抽象类中用abstract修饰的方法,只有方法签名,无函数体,子类必须实现;
  • 核心作用是统一子类的接口规范,同时复用一些通用逻辑

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 抽象类:用 abstract 修饰,不能直接实例化
abstract class Shape {
// 普通属性:可复用(有具体值)
public color: string;

constructor(color: string) {
this.color = color;
}

// 普通方法:有具体实现,子类可直接复用
public showColor() {
console.log(`这个图形的颜色是:${this.color}`);
}

// 抽象方法:只有签名,无函数体,子类必须实现
abstract getArea(): number; // 计算面积
abstract getPerimeter(): number; // 计算周长
}

// 子类:圆形(继承抽象类 Shape)
class Circle extends Shape {
// 子类独有的属性
private radius: number;

constructor(color: string, radius: number) {
// 调用父类构造函数(抽象类也有构造函数)
super(color);
this.radius = radius;
}

// 必须实现抽象方法1:计算圆的面积
getArea(): number {
return Math.PI * this.radius **2;
}

// 必须实现抽象方法2:计算圆的周长
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}

类实现接口

关键字implements:遵守规范,约束类的实现

接口 VS 抽象类

  • 接口interface:仅定义规范,无任何逻辑,class可以implements多个接口
  • 抽象类:既定义规范(抽象方法),又提供可复用逻辑(普通方法)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接口:仅定义“契约”(属性/方法签名),无具体实现
interface Flyable {
speed: number;
fly(): void; // 只定义方法签名,无函数体
}

// 类:implements 实现 Flyable 接口
class Bird implements Flyable {
// 必须实现接口的所有属性
speed: number;

constructor(speed: number) {
this.speed = speed;
}

// 必须实现接口的所有方法(函数体自己写)
fly() {
console.log(`鸟以 ${this.speed} km/h 的速度飞行`);
}
}

// 使用
const sparrow = new Bird(30);
sparrow.fly(); // 鸟以 30 km/h 的速度飞行

泛型类

在类名后用 声明 “类型变量”,类的属性、方法、构造函数可使用这个类型变量,实例化时再传入具体类型

类型收窄

类型收窄:通过代码逻辑(如条件判断)让 TS 自动推导变量的更具体类型(比如从 unknown 收窄到 string,从 string | number 收窄到 string),本质是缩小类型范围

方法 适用场景 示例
typeof 原始类型(string/number/boolean…) if (typeof x === "string") { x.length }
instanceof 类/构造函数创建的对象(Array、Date、自定义类) if (x instanceof Array) { x.push(1) }
in 对象属性存在判断,区分不同对象类型 if ("name" in x) { console.log(x.name) }
等值判断 字面量、联合类型(=== / !==) if (x === "admin") { ... }
类型守卫函数 自定义接口/复杂对象 function isUser(x: unknown): x is User { ... }
真值判断 排除 null/undefined/假值 if (x) { x.doSomething() }

自定义类型守卫函数

类型守卫函数:自定义收窄逻辑
通过返回 value is Type 的自定义函数(类型守卫),TS 会按照函数逻辑收窄类型(最灵活的场景)。

1
2
3
4
5
6
7
8
9
10
11
12
// 定义类型守卫函数:判断是否为数字
function isNumber(value: unknown): value is number {
return typeof value === "number" && !isNaN(value);
}

// 初始类型:unknown
let val: unknown = 123;

// 使用类型守卫
if (isNumber(val)) {
console.log(val * 2); // ✅ 收窄为 number
}

更好的类型收窄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
interface User {
name: string;
age: number;
occupation: string;
}

interface Admin {
name: string;
age: number;
role: string;
}

export type Person = User | Admin;

export const persons: Person[] = [
{
name: 'Max Mustermann',
age: 25,
occupation: 'Chimney sweep'
},
{
name: 'Jane Doe',
age: 32,
role: 'Administrator'
},
{
name: 'Kate Müller',
age: 23,
occupation: 'Astronaut'
},
{
name: 'Bruce Willis',
age: 64,
role: 'World saver'
}
];

export function logPerson(person: Person) {
let additionalInformation: string;
if (person.role) { // ❌ 应该改为 'role' in person来判断,这种方式下ts才会组做类型收窄
additionalInformation = person.role;
} else {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}

persons.forEach(logPerson);

特殊类型

交叉类型(&)

简单理解就是合并两个属性

  • 若属性是基本类型:结果为两个类型的交叉(通常是 never)
  • 若属性是对象类型:会递归合并该属性的类型

联合类型(|)

允许一个变量是多种类型中的一种

never类型

never 代表永远不会有返回值、永远不会被执行到 或 永远无法匹配的类型,是 TS 中 “空” 的极致表达。

  1. 场景:抛出错误的函数:永远不会正常返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function throwError(message: string): never {
    throw new Error(message);
    // 后面的代码永远执行不到,返回值类型是 never
    }

    // 2. 无限循环的函数:永远不会结束
    function infiniteLoop(): never {
    while (true) {}
    }
  2. 场景:联合类型的 “空子集”

    1
    2
    3
    4
    5
    // 1. 互斥类型交叉 → never(没有值能同时是 string 和 number)
    type Impossible = string & number; // never

    // 2. 空联合类型 → never
    type EmptyUnion = never | never; // never

unknown类型

unknown 类型可以理解成是一个更安全的 any 类型,任何类型可赋值给 unknown
与 any 不同的是,在将 unknown 类型赋值给其他类型之前,必须先进行类型检查或类型断言

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 模拟接口请求函数(返回值类型未知)
async function fetchData(url: string): Promise<unknown> {
const res = await fetch(url);
return res.json(); // 响应体可能是任意类型
}

// 安全处理返回值
async function handleData() {
const data = await fetchData("/api/user");

// 第一步:判断是否是对象(排除 null)
if (typeof data === "object" && data !== null) {
// 第二步:判断是否包含 name 属性
if ("name" in data && typeof (data as { name: unknown }).name === "string") {
// 安全访问 name 属性
console.log("用户名:", (data as { name: string }).name);
}

// 第三步:判断是否包含 age 属性
if ("age" in data && typeof (data as { age: unknown }).age === "number") {
console.log("年龄:", (data as { age: number }).age);
}
}
}

映射类型

基本语法:{ [K in Keys]: T }
其实就类似 js 中的 for in语句,用于遍历 Keys 中的所有类型

比较常见的语法

1
2
3
4
5
6
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }

利用映射类型实现的utility type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface Person {
   name: string;
   age: number;
   location: string;
}

type LazyPerson = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }

条件类型

基本语法:T extends U ? X : Y

常见用法:

1
2
3
4
5
6
type IsString<T> = T extends string ? true : false;

type I0 = IsString<number>;  // false
type I1 = IsString<"abc">;  // true
type I2 = IsString<any>;  // boolean
type I3 = IsString<never>;  // never

特殊关键字

extends关键字

主要使用场景

  • 类继承
  • 接口继承
  • 泛型约束
  • 条件类型

泛型约束使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
// 约束 K 必须是 T 的键(keyof T 是 T 所有键的联合类型)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const product = { name: "手机", price: 2999, stock: 100 };
// ✅ K 是 "price"(属于 keyof product)
getProperty(product, "price"); // 2999

// ❌ K 是 "color"(不属于 keyof product)
getProperty(product, "color");
// 报错:Argument of type '"color"' is not assignable to parameter of type '"name" | "price" | "stock"'

infer关键字

infer = 类型层面的 “变量”,仅能在 T extends U ? X : Y 这种条件类型中使用
作用是「捕获 / 提取」复杂类型的某一部分(比如函数返回值、数组元素类型),并给这部分类型起一个变量名供后续使用

提取数组元素类型
1
2
type ArrayItem<T> = T extends (infer U)[] ? U : never;
// 这里T extends (infer U)[] ? U : never;的意思是,如果T是某个待推断类型的数组,则返回推断的类型,否则返回never
提取函数返回值类型
1
2
3
4
5
type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;

// 使用
type Fn = () => { name: string; age: number };
type FnReturn = GetReturn<Fn>; // { name: string; age: number }
React 中infer的使用

useReducer 举例,如果我们这样使用 useReducer

1
2
3
const reducer = (x: number) => x + 1;
const [state, dispatch] = useReducer(reducer, '');
// Argument of type "" is not assignable to parameter of type 'number'.

这里useReducer会报一个类型错误,说””不能赋值给number类型

那React这里是如何通过reducer函数的类型来判断state的类型的?

直接看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function useReducer<R extends Reducer<any, any>, I>(
 reducer: R,
 // ReducerState 推断类型
 initializerArg: I & ReducerState<R>,
 initializer: (arg: I & ReducerState<R>) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

// infer推断
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any>
 ? S
: never;
// Reducer类型
type Reducer<S, A> = (prevState: S, action: A) => S;

in关键字

特殊操作符

! 非空断言操作符

+- 修饰符

Ts challenge

字段全改字符串

1
2
3
4
5
6
7
8
type Demo1 = {
a: number;
b: number;
}

type NewDemo1 = {
[P in keyof Demo1]: string;
}

字段全改函数,函数返回值是字段原本类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Demo1 = {
name: number;
age: string;
}

type NewDemo1 = {
[P in keyof Demo1]: () => Demo1[P];
}

// 改成 {getName: () => number, getAge: () => string} 这种形式
type NewDemo1 = {
[P in keyof Demo1 as `get${Capitalize<P>}`]: () => Demo1[P];
}

实现一个Optional(把一个类型里的部分字段变为可选)

1
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

实现一个GetOptional(得到某个类型里面所有可选字段)

1
type GetOptional<T> = [P in keyof T]

只保留对象类型中值为 string 类型的键值对

1
2
3
type PickStringValues<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}

TypeScript简介

  1. TypeScript是JavaScript的超集。
  2. 它对JS进行了扩展,向JS中引入了类型的概念,并添加了许多新的特性。
  3. TS代码需要通过编译器编译为JS,然后再交由JS解析器执行。
  4. TS完全兼容JS,换言之,任何的JS代码都可以直接当成JS使用。
  5. 相较于JS而言,TS拥有了静态类型,更加严格的语法,更强大的功能;TS可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率;TS代码可以编译为任意版本的JS代码,可有效解决不同JS运行环境的兼容问题;同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS却远远胜于JS。

TypeScript 开发环境搭建

下载Node.js
安装Node.js
使用npm全局安装typescript
  • 进入命令行
  • 输入:npm i -g typescript
创建一个ts文件
使用tsc对ts文件进行编译
  • 进入命令行
  • 进入ts文件所在目录
  • 执行命令:tsc xxx.ts
阅读全文 »

useEffect

执行副作用,一般情况下会在浏览器绘制页面后异步调用

useEffect() 的用途

只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种。

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

useEffect返回值

副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。

useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。

1
2
3
4
5
6
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]);

上面例子中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

阅读全文 »

Redux

设计思想

  • (1)Web 应用是一个状态机,视图与状态是一一对应的。
  • (2)所有的状态,保存在一个对象里面。

Action

  1. Action 就是 View 发出的通知,表示 State 应该要发生变化了。

  2. Action 是一个对象,其中的type属性是必须的,表示 Action 的名称,Action 描述当前发生的事情。

  3. 改变 State 的唯一办法,就是使用 Action。

可以定义一个函数来生成 Action,这个函数就叫 Action Creator,示例如下

1
2
3
4
5
6
7
8
9
10
const ADD_TODO = '添加 TODO';

function addTodo(text) {
return {
type: ADD_TODO,
text
}
}

const action = addTodo('Learn Redux');
阅读全文 »

Chrome浏览器架构

Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
image-20210930012419754

Browser Process

  • 子进程的管理(网络、渲染、GPU)
  • 负责包括地址栏,书签栏,前进后退按钮等部分的工作;
  • 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;

Network Service

  • 负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务

Renderer Process

  • 负责一个 tab 内关于网页呈现的所有事情,进程启动后,会开启一个渲染主线程,负责执行HTML、CSS、JavaScript代码

Plugin Process

  • 负责控制一个网页用到的所有插件,如 flash

GPU Process

  • 负责处理 GPU 相关的任务

由于一个tab标签页都有一个独立的渲染进程,所以一个tab异常崩溃后,其他tab不会受到影响。

一个渲染进程包括

  • 主线程
  • HTTP请求线程
  • 定时触发线程
  • 事件触发线程
  • GUI线程

渲染进程中的主线程是如何工作的

包括但不限于

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面绘制60次
  • 执行全局js代码
  • 执行事件处理函数
  • 执行计时器回调函数

浏览器渲染页面过程

浏览器拿到 HTML 后,并不是一次性把页面画出来,而是经历一条流水线:

1. 解析 HTML,构建 DOM Tree

  • 字节流 -> 字符 -> Token -> Node,最终形成 DOM 树。
  • 解析过程中遇到同步 script 可能会暂停 DOM 构建,先执行 JS。

2. 解析 CSS,构建 CSSOM Tree

  • 下载并解析样式,形成 CSSOM(CSS Object Model)。
  • CSS 解析通常不会阻塞 DOM 解析,但会影响后续渲染阶段。

3. 合并 DOM + CSSOM,生成 Render Tree

  • Render Tree 只包含需要显示的可见节点(如 display: none 不会进入)。
  • 每个可见节点会绑定其计算后的样式信息。

4. Layout(回流 / 重排)

  • 根据视口和样式计算每个节点的几何信息(位置、尺寸)。
  • 这个阶段会产出盒模型坐标数据。

5. Paint(重绘)

  • 把文字、颜色、边框、阴影等绘制成一条条绘制指令(Paint Records)。

6. Layer(分层)

  • 浏览器会根据元素特性把页面拆成多个图层(并不是每个元素都独立成层)。
  • 分层后,后续某些变化只需要更新局部图层,不必整页重绘。

7. Raster(栅格化)

  • 把图层的绘制指令转换为位图纹理(tiles),通常由栅格线程/GPU 参与完成。

8. Composite(合成)

  • 合成线程按图层顺序、透明度、变换矩阵把多个纹理合成,最终显示到屏幕上。

简化理解:DOM + CSSOM -> Render Tree -> Layout -> Paint -> Layer -> Raster -> Composite

常见触发与性能影响

  • 修改几何属性(如 widthheightmargin)更容易触发 Layout + Paint + Composite
  • 修改非几何属性(如 colorbackground)通常触发 Paint + Composite
  • 修改 transformopacity 通常只需 Composite,代价更低。
  • 主线程被长时间 JS 占用时,渲染会被阻塞,页面会出现掉帧或卡顿。

哪些场景更容易触发分层

  • 使用了 3D/变换相关属性(如 transformperspective)。
  • 使用 opacity 动画、will-change 或可能触发独立合成层的特性。
  • position: fixed / stickyvideocanvas 等元素在特定场景下也可能被提升为图层。

注意:分层不是越多越好。图层过多会增加内存占用和合成开销,需要在流畅度和资源之间权衡。

浏览器JS异步执行原理

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
  4. 主线程不断重复上面的第三步。
阅读全文 »

什么是函数式编程?

函数式编程具有五个鲜明的特点。

1. 函数是”第一等公民”

指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

举例来说,下面代码中的print变量就是一个函数,可以作为另一个函数的参数。

1
2
3
  var print = function(i){ console.log(i);};

  [1,2,3].forEach(print);

2. 只用”表达式”,不用”语句”

“表达式”(expression)是一个单纯的运算过程,总是有返回值;”语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。”语句”属于对系统的读写操作,所以就被排斥在外。

当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

阅读全文 »

React DevTools

General 设置

image-20220222024702149.png

General 面板中最重要的功能就是 **”Highlight updates when components render”**。勾选上之后,可以查看 React 重绘时,页面哪些部分有更新。在遇到性能问题时,可以快速帮助决策在哪部分不需要重绘的组件部分添加 React.memo 阻止重绘。

Debugging 设置

image-20220222024832097

“Append components stacks to console warnings and errors.”

可以帮助我们定位 React 报错信息来自哪个组件

阅读全文 »

HTTP相关,状态码,HTTP1/2/3、简单请求和非简单请求

HTTP 有哪些常见状态码

2xx(成功状态码)

200 OK

请求成功,服务器已成功处理了客户端的请求,并返回了请求的内容。

3xx(重定向状态码)

301 Moved Permanently

被请求的资源已永久移动到新位置,服务器返回这个状态码时,会在响应头的 Location 字段给出资源的新 URL。搜索引擎会更新其索引中的链接。

应用场景:如果一个网站更改了页面的 URL 结构,希望将旧的 URL 永久重定向到新的 URL

302 Found

表示请求的资源临时移动到了新位置。与 301 不同的是,搜索引擎不会更新索引中的链接

304 Not Modified

说明无需再次传输请求的内容,也就是说可以使用缓存的内容

阅读全文 »

HTTP缓存过程

第一次发送请求 - 服务器响应,响应标头带上Cache-Control、Expires、ETag、Last-Modified
第二次发送请求 - 先根据Cache-Control、Expires判断强缓存是否失效,如果失效,If-None-Match中的上一次ETag和If-Modified-Since中的上一次Last-Modified来判断是否变更,如果没有变更,返回304让浏览器使用本地缓存

image-20210808204737157

强缓存

Cache-Control

Cache-Control 是 HTTP/1.1 中新增的属性,在请求头和响应头中都可以使用,常用的属性值如有:

  • max-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效
  • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
  • no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源
  • private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应
  • public:响应可以被中间代理、CDN 等缓存
  • must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证
Expires

Expires 的值是一个 HTTP 日期,在浏览器发起请求时,会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。

由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。Expires 的优先级在三个 Header 属性中是最低的。

协商缓存

当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。

ETag/If-None-Match

ETag/If-None-Match 的值是一串 hash 码,代表的是一个资源的标识符,当服务端的文件变化的时候,它的 hash码会随之改变

Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间

如何实现ETag?

Nginx中ETag是如何实现的: 上次修改时间 + content-length

缓存配置

题目:简述你们前端项目中资源的缓存配置策略
题目:现代前端应用应如何配置 HTTP 缓存机制
关于 http 缓存配置的最佳实践为以下两条:

  • 文件路径中带有 hash 值:一年的强缓存。因为该文件的内容发生变化时,会生成一个带有新的 hash 值的 URL。前端将会发起一个新的 URL 的请求。配置响应头 Cache-Control: public,max-age=31536000,immutable
  • 文件路径中不带有 hash 值:协商缓存。大部分为 public 下文件。配置响应头 Cache-Control: no-cacheetag/last-modified

但是当处理永久缓存时,切记不可打包为一个大的 bundle.js,此时一行业务代码的改变,将导致整个项目的永久缓存失效,此时需要按代码更新频率分为多个 chunk 进行打包,可细粒度控制缓存。

HTTP缓存策略

  1. webpack-runtime: 应用中的 webpack 的版本比较稳定,分离出来,保证长久的永久缓存
  2. react/react-dom: react 的版本更新频次也较低
  3. vendor: 常用的第三方模块打包在一起,如 lodash,classnames 基本上每个页面都会引用到,但是它们的更新频率会更高一些。另外对低频次使用的第三方模块不要打进来
  4. pageA: A 页面,当 A 页面的组件发生变更后,它的缓存将会失效
  5. pageB: B 页面
  6. echarts: 不常用且过大的第三方模块单独打包
  7. mathjax: 不常用且过大的第三方模块单独打包
  8. jspdf: 不常用且过大的第三方模块单独打包

Nginx配置缓存

通常是通过Nginx来托管静态资源,缓存、跨域等配置也是在Nginx中进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# nginx.conf 核心配置
server {
listen 80;
server_name your-domain.com; # 你的域名

# 1. 静态资源根目录(前端打包后的 dist 目录)
root /usr/share/nginx/html;
index index.html;

# 2. 缓存配置(不同资源不同缓存策略)
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires 7d; # 强缓存:7
add_header Cache-Control "public, max-age=604800"; # 协商缓存
add_header ETag $etag; # 生成文件唯一标识

# 3. Gzip 压缩(文本资源必开)
gzip on;
gzip_types application/javascript text/css text/html;
gzip_min_length 1k; # 大于1k的文件才压缩

# 4. 防盗链(仅允许自己的域名访问)
valid_referers your-domain.com *.your-domain.com;
if ($invalid_referer) {
return 403; # 非允许域名返回403
}
}

# 5. 单页应用路由适配(history 模式)
location / {
try_files $uri $uri/ /index.html; # 找不到文件时返回 index.html
}

# 6. 重定向到 HTTPS(可选)
return 301 https://$host$request_uri;
}

# HTTPS 配置(可选)
server {
listen 443 ssl;
server_name your-domain.com;

ssl_certificate /path/to/your/cert.pem; # SSL 证书路径
ssl_certificate_key /path/to/your/key.pem;

# 其余配置和上面一致(根目录、缓存、压缩等)
root /usr/share/nginx/html;
index index.html;
}
0%