阅读视图

发现新文章,点击刷新页面。

Hello,算法:微博热搜的背后

每天一睁眼,新闻头条、微博热搜都能抓住很多人的目光,购物时也会有商品排行榜,它们无形中对我们的注意力和决策产生重要影响。 那么,生活中随处可见的排行榜,在程序里如何做到,算法又叫什么呢?

Vue-Vue2中的Mixin 混入机制

在开发中,当多个组件拥有相同的逻辑(如分页、导出、权限校验)时,重复编写代码显然不够优雅。Vue 2 提供的 Mixin(混入)正是为了解决逻辑复用而生。本文将从基础用法出发,带你彻底理清 Mixin

既然有了 defer,我们还需要像以前那样把 <script>标签放到 <body>的最底部吗?

既然有了 defer,我们还需要像以前那样把 <script> 标签放到 <body> 的最底部吗?如果我把带 defer 的脚本放在 <head> 里,会有性能问题吗?

核心答案

不需要了。 使用 defer 属性后,把 <script> 放在 <head> 里不仅没有性能问题,反而是更优的做法

原因:

  1. defer 脚本会并行下载,不阻塞 HTML 解析
  2. 脚本执行会延迟到 DOM 解析完成后,但在 DOMContentLoaded 事件之前
  3. 放在 <head> 里可以让浏览器更早发现并开始下载脚本

深入解析

浏览器解析机制

传统 <script>(无 defer/async):
HTML 解析 ──▶ 遇到 script ──▶ 暂停解析 ──▶ 下载脚本 ──▶ 执行脚本 ──▶ 继续解析

defer 脚本:
HTML 解析 ────────────────────────────────────────────▶ DOM 解析完成 ──▶ 执行脚本
     └──▶ 并行下载脚本 ──────────────────────────────────────────────────┘

为什么 <head> 里的 defer 更好?

位置 发现脚本时机 开始下载时机
<head> 解析开始时 立即
<body> 底部 解析接近完成时 较晚

放在 <head> 里,浏览器可以在解析 HTML 的同时下载脚本,充分利用网络带宽

常见误区

误区 1: "defer 脚本放 <head> 会阻塞渲染"

  • 错误。defer 脚本的下载和 HTML 解析是并行的

误区 2: "放 <body> 底部更保险"

  • 这是 defer 出现之前的最佳实践,现在已过时
  • 放底部反而会延迟脚本的发现和下载

误区 3: "defer 和放底部效果一样"

  • 不一样。放底部时,脚本下载要等到 HTML 解析到那里才开始
  • defer 在 <head> 里可以更早开始下载

defer vs async vs 传统方式

                    下载时机        执行时机              执行顺序
传统 script         阻塞解析        下载完立即执行         按文档顺序
async              并行下载        下载完立即执行         不保证顺序
defer              并行下载        DOM 解析完成后        按文档顺序

代码示例

<!-- ✅ 推荐:defer 脚本放在 <head> -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>页面标题</title>
  <!-- 浏览器立即发现并开始下载,但不阻塞解析 -->
  <script defer src="vendor.js"></script>
  <script defer src="app.js"></script>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <!-- HTML 内容 -->
</body>
</html>

<!-- ❌ 过时做法:放在 body 底部 -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>页面标题</title>
</head>
<body>
  <!-- HTML 内容 -->

  <!-- 要等 HTML 解析到这里才开始下载 -->
  <script src="vendor.js"></script>
  <script src="app.js"></script>
</body>
</html>

验证下载时机的方法

打开 Chrome DevTools → Network 面板,观察脚本的下载开始时间:

  • <head> 里的 defer 脚本:在 HTML 下载初期就开始
  • <body> 底部的脚本:在 HTML 解析接近完成时才开始

面试技巧

可能的追问方向

  1. "defer 和 async 有什么区别?"

    • async 下载完立即执行,不保证顺序
    • defer 等 DOM 解析完才执行,保证顺序
  2. "多个 defer 脚本的执行顺序是怎样的?"

    • 按照在文档中出现的顺序执行
    • 即使后面的脚本先下载完,也会等前面的
  3. "defer 脚本和 DOMContentLoaded 的关系?"

    • defer 脚本在 DOM 解析完成后、DOMContentLoaded 触发前执行
  4. "什么情况下还是要放 body 底部?"

    • 需要兼容不支持 defer 的古老浏览器(IE9 以下)
    • 现代开发中基本不需要考虑

展示深度的回答方式

"defer 放 <head> 不仅没有性能问题,反而是更优的选择。因为浏览器的预加载扫描器(Preload Scanner)可以在解析 HTML 的早期就发现这些脚本并开始下载,充分利用网络带宽。而放在 <body> 底部的话,脚本的发现时机会延后,相当于浪费了并行下载的机会。"

一句话总结

defer 脚本放 <head> 是现代最佳实践:更早发现、并行下载、不阻塞解析、按序执行。

我的状态管理哲学

背景 React状态管理的演进始终围绕“组件通信”与“状态复用”两大核心需求展开。 早期类组件时代,开发者依赖props实现组件间传值,通过state管理组件内部状态,但跨层级组件通信需借助“prop

从N倍人力到1次修改:Vite Plugin Modular 如何拯救多产品前端维护困境

0. 引言:一个真实的故事

产品经理:"我们需要在所有产品中添加一个新的用户反馈功能,下周上线!"

前端开发:"😰 我们有8个产品,每个都需要单独修改,这得加班到什么时候啊..."

技术总监:"🤔 这样下去不是办法,维护成本太高了。我们需要一个更好的解决方案!"

隔壁老王:"我来!用 Vite Plugin Modular,一次修改,所有产品自动同步更新!"

前端开发:"真的假的?这么神奇?"

技术总监:"哇哦!(兴奋)好厉害!(星星眼🌟)这就是我们需要的!(一脸崇拜😍)"


1. 项目背景与痛点

在我们公司的实际业务中,随着业务的快速发展,我们面临着一个具体的挑战:公司拥有许多不同的产品,但各产品都有类似的功能模块。传统的实现方案是为每个产品创建独立的前端项目,各自维护对应的功能。这种方式在初期可能运行良好,但随着业务的不断扩展,问题逐渐凸显:

  • 维护成本高:功能变更需要在多个项目中重复实现,耗费N倍的人力。例如,当需要修改一个通用的登录功能时,需要在所有产品的前端项目中逐一修改,不仅耗时耗力,还容易出现遗漏。
  • 代码冗余:相似功能在不同项目中重复编写,导致代码库臃肿,增加了存储和维护成本。
  • 一致性难以保证:不同项目的相同功能可能出现实现差异,导致用户在使用不同产品时体验不一致,影响品牌形象。
  • 部署和配置复杂:每个项目都需要独立的部署流程和配置管理,增加了DevOps团队的工作负担。
  • 团队协作效率低:开发者需要在多个项目间切换,增加了上下文切换成本,降低了开发效率。
  • 技术债务累积:随着时间推移,各项目可能采用不同的技术栈和实现方式,导致技术债务不断累积,难以统一升级和维护。

为了解决这些实际业务问题,我们开发了 Vite Plugin Modular,一个专为多模块、多环境前端项目设计的 Vite 插件。它通过将多个相关产品的功能模块整合到单个项目中,实现了代码的复用和统一管理,大幅降低了维护成本,提高了开发效率,为公司的业务发展提供了更灵活、更高效的前端技术支持。

2. 核心功能介绍

2.1 多模块管理

Vite Plugin Modular 允许在单个项目中管理多个独立的功能模块,每个模块都有自己的源码目录、入口文件和配置。通过命令行工具,开发者可以轻松添加、删除和管理模块:

  • 模块化目录结构:自动生成标准化的模块目录结构,保持代码组织清晰
  • 独立的模块配置:每个模块可以有自己的标题、入口文件、输出目录等配置
  • 模块间隔离:模块间相互独立,避免命名冲突和代码耦合

2.2 多环境配置

针对每个模块,Vite Plugin Modular 支持配置多个环境(如 development、production、test 等),实现环境的精细化管理:

  • 环境变量注入:自动将配置的环境变量注入到代码中,可通过 import.meta.env 访问
  • 环境特定配置:为不同环境提供不同的配置,满足各种部署场景需求
  • 统一的环境管理:通过命令行工具方便地添加和删除环境配置

2.3 命令行工具

提供了功能强大的命令行工具(vmod),简化模块和环境的管理:

  • 模块管理命令adddeletelist 等命令用于模块的生命周期管理
  • 环境管理命令addEnvdeleteEnv 等命令用于环境配置的管理
  • 配置管理命令config 命令用于修改模块配置
  • 智能命令生成:自动为每个模块和环境生成对应的 npm 脚本命令

2.4 智能构建系统

Vite Plugin Modular 集成了智能的构建系统,为每个模块和环境提供定制化的构建配置:

  • 动态入口解析:根据当前模式自动解析模块入口路径
  • 自定义输出目录:每个模块可以配置独立的输出目录
  • HTML 自动转换:替换 HTML 页面标题和入口脚本为模块配置的值
  • 构建优化:继承 Vite 的优秀构建性能,同时提供模块级别的优化

2.5 环境变量处理

提供了灵活的环境变量处理机制,简化配置管理:

  • 自动环境变量注入:将配置的环境变量转换为 VITE_ 前缀的环境变量
  • 命名规范转换:自动将驼峰命名转换为蛇形命名,保持环境变量命名一致性
  • 模块特定环境变量:每个模块可以有自己的环境变量配置

3. 技术选型理由

3.1 方案选型对比

在设计 Vite Plugin Modular 之前,我们评估了多种前端多模块开发方案,包括:

方案 优势 劣势
npm 组件库 • 代码复用性高 • 版本管理清晰 • 可跨项目使用 • 发布流程繁琐 • 调试不便 • 依赖管理复杂 • 无法共享完整页面级功能 • 不适用于经常变更的业务需求
Monorepo • 代码集中管理 • 版本统一管理 • 跨包依赖便捷 • 初始设置复杂 • 构建时间长 • 学习成本高 • 配置繁琐
Vite 多页 • 配置简单 • 共享依赖 • 构建性能好 • 页面级隔离,无法实现模块级隔离 • 环境配置管理复杂 • 缺乏统一的模块管理工具 • 开发环境 URL 需要指定到具体的 HTML 文件
Vite Plugin Modular • 模块级隔离 • 多环境配置 • 命令行工具支持 • 快速开发构建 • 统一管理与代码复用 • 依赖 Vite 生态 • 对单模块项目优势不明显

3.2 基于 Vite

选择 Vite 作为基础构建工具,主要考虑以下因素:

  • 快速的开发服务器:Vite 的开发服务器采用原生 ESM,启动速度极快,适合多模块开发场景
  • 优化的构建性能:使用 Rollup 进行生产构建,提供优秀的代码分割和 tree-shaking
  • 丰富的插件生态:Vite 拥有活跃的插件生态系统,便于扩展功能
  • 现代前端特性支持:内置对 TypeScript、JSX、CSS 预处理器等的支持
  • 环境变量处理:Vite 内置了环境变量处理机制,与我们的需求高度契合

3.3 TypeScript 开发

采用 TypeScript 进行开发,带来以下优势:

  • 类型安全:提供静态类型检查,减少运行时错误
  • 更好的 IDE 支持:TypeScript 提供了更强大的代码补全和类型提示
  • 可维护性:类型定义使代码更易于理解和维护
  • 更好的重构支持:类型系统使重构更加安全和高效

3.4 命令行工具选型

命令行工具采用以下技术栈:

  • Commander.js:用于解析命令行参数和选项
  • Inquirer.js:提供交互式命令行界面,提升用户体验
  • Chalk:用于终端彩色输出,提高日志可读性
  • Node.js 文件系统 API:用于文件和目录的操作

命令行工具效果展示

4. 实现原理与流程

4.1 核心工作流程

Vite Plugin Modular 的核心工作流程如下所示,通过 Vite 插件机制,在构建过程中动态解析模块和环境信息,实现模块化的配置管理和构建流程。

4.1.1 模块解析机制

  1. 模式解析:通过 Vite 的 mode 参数,解析模块和环境信息。例如,当运行 vite --mode module1-dev 时,插件会自动解析出模块名称为 module1,环境为 dev
  2. 配置加载:根据解析出的模块名称,加载对应的模块配置。配置文件采用 JSONC 格式,支持注释,提高了可读性和可维护性。
  3. 路径转换:根据模块配置,动态转换入口文件路径和输出目录路径。例如,将 src/main.ts 转换为 src/modules/module1/main.ts,将输出目录设置为 dist/module1
  4. HTML 处理:通过 Vite 的 HTML 转换钩子,替换页面标题和入口脚本为模块配置的值,确保每个模块都有正确的页面标题和入口点。

4.1.2 环境变量处理

  1. 变量注入:将模块配置中的 define 字段转换为环境变量,通过 Vite 的 define 选项注入到代码中。例如,将 { "apiUrl": "https://api.example.com" } 转换为 import.meta.env.VITE_API_URL
  2. 命名规范:自动将驼峰命名转换为蛇形命名,保持环境变量命名的一致性。例如,将 apiUrl 转换为 VITE_API_URL
  3. 环境覆盖:支持环境特定的变量覆盖,确保不同环境可以使用不同的变量值。

4.2 命令行工具实现

命令行工具(vmod)的实现基于以下核心流程:

  1. 命令注册:使用 Commander.js 注册各种模块管理命令,如 adddeletelistaddEnvdeleteEnv 等。
  2. 交互式界面:使用 Inquirer.js 实现交互式命令行界面,在用户执行命令时提供智能提示和选择。
  3. 文件操作:使用 Node.js 文件系统 API 进行文件和目录的操作,如创建模块目录、生成配置文件、复制模板文件等。
  4. 配置管理:实现配置文件的读取、修改和写入,确保模块配置的一致性和完整性。
  5. 命令生成:在添加模块或环境时,自动生成对应的 npm 脚本命令,方便用户运行和构建模块。

4.3 与 Vite 的集成

Vite Plugin Modular 与 Vite 的集成主要通过以下钩子实现:

  1. config:在 Vite 配置阶段,修改配置对象,设置正确的入口文件路径、输出目录路径和环境变量。
  2. transformIndexHtml:在 HTML 转换阶段,替换页面标题和入口脚本为模块配置的值,确保每个模块都有正确的页面标题和入口点。

这两个钩子是实际实现中使用的核心钩子,通过它们实现了模块解析、配置加载、路径转换和 HTML 处理等核心功能。

4.4 模块隔离机制

Vite Plugin Modular 实现了模块间的隔离,确保各模块之间相互独立,避免代码冲突和依赖混乱:

  1. 目录隔离:每个模块都有自己的目录,独立存放源码和资源文件。
  2. 配置隔离:每个模块都有自己的配置,支持不同的入口文件、输出目录和环境变量。
  3. 依赖隔离:各模块共享项目级的依赖,但可以通过条件导入实现模块特定的依赖。
  4. 构建隔离:每个模块的构建过程相互独立,避免构建过程中的相互影响。

5. 与传统多项目方案的对比

针对公司多产品、功能重复的场景,Vite Plugin Modular 与传统的多项目方案相比具有显著优势:

特性 传统多项目方案 Vite Plugin Modular
项目结构 多个独立项目,各自维护 单个项目多模块结构,集中管理
功能变更 需要在多个项目中重复实现,耗费N倍人力 集中修改,所有模块自动同步更新
代码复用 复制粘贴或通过 npm 包共享,复用成本高 项目内直接共享代码,复用成本低
一致性保证 不同项目可能出现实现差异,用户体验不一致 统一实现,确保所有产品功能一致性
开发流程 多项目切换,上下文切换成本高 单项目内开发,流程简化
部署管理 每个项目独立部署,配置复杂 统一部署配置,模块化部署
环境配置 每个项目独立管理环境变量 统一环境管理,模块化配置
构建性能 每个项目独立构建,构建时间长 共享构建配置,优化构建性能
学习成本 新成员需要熟悉多个项目结构 只需熟悉一个项目结构和模块配置
扩展性 新增产品需要创建新项目,周期长 新增模块即可,快速响应业务需求

5. 快速开始指南

5.1 安装

# 使用 npm
npm install @ad-feiben/vite-plugin-modular --save-dev

# 使用 yarn
yarn add @ad-feiben/vite-plugin-modular -D

# 使用 pnpm
pnpm add @ad-feiben/vite-plugin-modular -D

5.2 配置

  1. 初始化配置
# 使用 CLI 命令初始化
npx vmod init

# 或使用简写
npx vm init

2. 在 vite.config.ts 中注册插件

import { defineConfig } from 'vite'
import VitePluginModular from '@ad-feiben/vite-plugin-modular'

export default defineConfig({
  plugins: [
    VitePluginModular()
  ]
})

5.3 创建模块

以下是创建模块的流程图,展示了从执行命令到模块创建完成的完整过程:

以下是创建模块的实际效果展示:

5.4 开发和构建

创建模块后,Vite Plugin Modular 会自动生成对应的 npm 脚本命令:

# 运行特定模块的开发服务器
npm run dev:module1-dev

# 构建特定模块的生产版本
npm run build:module1-prod

5.5 目录结构

创建模块后,会自动生成以下目录结构,保持代码组织清晰:

src/modules/
├── module1/          # 模块目录
└── module2/
└── moduleN/

6. 适用场景

Vite Plugin Modular 特别适合以下场景:

6.1 多产品公司

对于拥有多个相关产品的公司,Vite Plugin Modular 可以将这些产品的前端代码整合到单个项目中,实现代码复用和统一管理。

6.2 微前端架构

在微前端架构中,Vite Plugin Modular 可以作为微前端模块的开发和构建工具,简化模块的管理和部署。

6.3 企业内部系统

企业内部通常有多个功能相关的系统(如 CRM、ERP、OA 等),Vite Plugin Modular 可以将这些系统的前端代码整合到单个项目中,提高开发和维护效率。

6.4 SaaS 产品

对于 SaaS 产品,不同客户可能有不同的定制需求,Vite Plugin Modular 可以通过模块和环境的配置,轻松实现不同客户的定制版本。

6.5 快速原型开发

在需要快速开发多个相关原型的场景中,Vite Plugin Modular 可以帮助开发者快速创建和管理多个原型模块,提高原型开发效率。

7. 未来规划

Vite Plugin Modular 是一个持续发展的项目,我们计划在未来的版本中添加以下功能:

7.1 国际化支持

  • 实现模块级别的国际化配置,支持不同模块使用不同的语言设置
  • 提供多语言资源管理系统,方便管理和维护多语言内容
  • 支持自动语言切换,根据用户环境或配置自动选择合适的语言

7.2 UI 界面

  • 开发可视化的模块管理界面,提供直观的模块创建、编辑、删除功能
  • 实现配置编辑器,通过图形界面编辑模块配置,减少手动编辑配置文件的错误
  • 提供实时预览功能,在修改配置后立即查看效果
  • 集成项目状态监控,显示模块构建状态、依赖关系等信息
  • 支持拖放操作,通过拖放方式管理模块间的依赖关系

7.3 完善文档

  • 编写详细的 API 文档,覆盖所有插件配置选项和命令行参数
  • 提供全面的使用指南,包括快速开始、高级配置、最佳实践等
  • 建立社区支持渠道,收集用户反馈和建议,持续改进插件功能

结语

Vite Plugin Modular 为前端多模块开发提供了一种全新的思路,通过将多个相关产品的功能模块整合到单个项目中,实现了代码的复用和统一管理,大幅降低了维护成本,提高了开发效率。它不仅是一个技术工具,更是一种前端工程化的最佳实践。

无论您是在开发多个相关产品,还是在构建微前端架构,Vite Plugin Modular 都能为您的项目带来显著的价值。我们相信,随着它的不断发展和完善,它将成为前端多模块开发的标准解决方案之一。

立即尝试 Vite Plugin Modular,体验前端模块化开发的新境界!

我的项目实战(九)—— 实现页面状态缓存?手写KeepAlive ,首页优化组件

今天,我们的项目要继续深入一个“看起来简单、实则暗流涌动”的功能场景:页面状态缓存 —— KeepAlive

你可能已经见过这样的需求:

“用户从首页点进详情页,再返回时,首页又要重新加载?能不能记住我之前滑到哪了?”

这不只是用户体验的问题,更是对前端架构的一次考验。


一、问题起点:为什么首页总在“重复劳动”?

在 React 单页应用中,路由切换并不会刷新页面,但组件会经历完整的挂载与卸载过程。

以常见的首页为例:

<Route path="/home" element={<Home />} />

当用户从 /home 切换到 /detail 时,React 会执行 Home.unmount()
再次返回时,则重新执行 Home.mount() —— 所有 useState 清零,useEffect 重跑,接口重发,列表重渲染。

结果就是:

  • 用户每次回来都要等数据加载;
  • 滚动位置回到顶部;
  • 已填写的搜索条件丢失;
  • 动画闪烁明显。

这不是 SPA 应该有的样子。我们需要的是:视觉上离开,逻辑上留下

于是,KeepAlive 出现了。


二、目标拆解:一个合格的 KeepAlive 要解决什么问题?

别急着引入第三方库,先明确我们的核心诉求:

  1. 组件状态保留:包括 state、ref、DOM 结构、滚动位置;
  2. 按需缓存:不是所有页面都需要缓存,要可配置;
  3. 内存可控:不能无限制缓存,避免内存泄漏;
  4. 与路由系统良好集成:支持 React Router 等主流方案;
  5. 组件卸载时自动清理资源:防止事件监听、定时器残留。

这些要求听起来像 Vue 的 <keep-alive>?没错,但在 React 中,它需要我们更主动地去构建这套机制。


三、方案选型:自研 vs 第三方库

方案一:手写简易版 KeepAlive

我们可以用最朴素的方式模拟缓存行为:

const [cache, setCache] = useState({});
const [activeKey, setActiveKey] = useState(null);

// 缓存当前组件
useEffect(() => {
  if (children && activeId) {
    setCache(prev => ({ ...prev, [activeId]: children }));
  }
}, [activeId, children]);

return (
  <>
    {Object.entries(cache).map(([key, comp]) => (
      <div key={key} style={{ display: key === activeKey ? 'block' : 'none' }}>
        {comp}
      </div>
    ))}
  </>
);

✅ 优点:

  • 原理清晰,适合教学理解;
  • 不依赖额外包,轻量;
  • 可完全掌控缓存策略。

❌ 缺点:

  • 无法真正保留组件实例(如 ref、内部 state 生命周期);
  • 子组件更新可能导致缓存失效;
  • 难以处理复杂嵌套结构;
  • 没有统一的缓存管理机制。

这种方式更适合静态内容或演示用途,不适合生产环境。


方案二:使用 react-activation

这是一个专门为 React 实现类似 Vue keep-alive 行为的成熟库。

它提供了三个核心能力:

import { AliveScope, KeepAlive } from 'react-activation';

function App() {
  return (
    <AliveScope>
      <Router>
        <Routes>
          <Route
            path="/home"
            element={
              <KeepAlive name="home" saveScrollPosition="screen">
                <Home />
              </KeepAlive>
            }
          />
        </Routes>
      </Router>
    </AliveScope>
  );
}

核心组件说明:

组件 作用
<AliveScope> 全局缓存容器,必须作为根节点包裹整个应用或需要缓存的部分
<KeepAlive> 包裹需要缓存的组件,通过 name 做唯一标识
useActivate/useUnactivate 替代 useEffect,监听组件激活/失活状态

✅ 真正做到了什么?

  • 组件卸载时不销毁实例,而是移入缓存池;
  • 再次激活时直接复用原有实例,state 完全保留;
  • 支持滚动位置记忆(saveScrollPosition);
  • 提供钩子函数控制数据刷新时机。

这才是我们想要的“活”的组件。


四、实践细节:如何安全高效地使用 KeepAlive?

1. 合理设置缓存粒度

不是所有页面都值得被缓存。比如:

  • 登录页、支付成功页这类一次性页面,不应缓存;
  • 数据强实时性页面(如股票行情),缓存反而会造成信息滞后。

✅ 建议只对以下类型启用:

  • 首页、推荐流、商品列表等高频访问页;
  • Tab 类布局中的子页面(可用 name 动态生成);
  • 用户常往返跳转的路径。
<KeepAlive name={`list_${category}`}>...</KeepAlive>

2. 控制数据更新节奏:useActivate 是关键

由于组件不会重新 mount,useEffect(() => {}, []) 只会在首次进入时触发一次。

这意味着:后续返回不会拉取最新数据

解决方案是使用专属钩子:

import { useActivate } from 'react-activation';

function Home() {
  const [data, setData] = useState([]);

  // 每次激活时执行
  useActivate(() => {
    console.log('Home 被唤醒');
    fetchLatestData().then(setData);
  });

  return <div>{/* 渲染内容 */}</div>;
}

这样既保留了状态,又能保证内容不过期。


3. 内存与性能的平衡

虽然 react-activation 做了很多优化,但我们仍需警惕:

  • 长期缓存大量组件会导致内存占用上升;
  • 特别是在移动端,内存资源有限。

📌 建议措施

  • 设置最大缓存数量(可通过封装中间层控制);
  • 对非活跃页面手动清除缓存(调用 dropByCacheKey);
  • 在开发工具中监控内存变化,及时发现问题。

4. 清理副作用:别忘了事件监听和定时器

即使组件被缓存,也不能放任副作用不管。

错误示例:

useEffect(() => {
  const timer = setInterval(polling, 5000);
  return () => clearInterval(timer); // ❌ 只在 unmount 时清理
});

如果组件一直被缓存,这个定时器将永远运行!

✅ 正确做法是结合 useUnactivate

useEffect(() => {
  const timer = setInterval(polling, 5000);
  return () => clearInterval(timer);
}, []);

// 或者使用专用钩子
useUnactivate(() => {
  console.log('Home 暂时休眠');
  // 可在此暂停轮询、断开 WebSocket 等
});

让组件在“休眠”前主动释放资源,醒来后再恢复。


五、总结:KeepAlive 是一种思维转变

KeepAlive 不只是一个技术组件,它代表了一种新的开发范式:

我们不再假设组件每次出现都是“全新”的,而要开始考虑它的“生命周期状态”

就像人离开房间又回来,不应该忘记自己刚才在做什么。

能力 在本组件中的体现
状态持久化 保留 scrollY、form 输入、局部状态
性能优化 避免重复渲染、减少网络请求
用户体验 返回即原样,无闪烁无等待
工程化思维 合理缓存、资源清理、可维护性

六、结语

前端开发的魅力就在于:
那些最容易被忽略的小功能,往往藏着最深的设计哲学。

从“回到顶部”到“页面缓存”,我们在一次次打磨中学会思考:

“用户真正需要的是什么?”
“我们是在做功能,还是在解决问题?”

KeepAlive 不是为了炫技,而是为了让用户感受到:这个页面记得我

下次当你接到“首页老是重新加载”的反馈时,不妨试试给它加一层 KeepAlive —— 让页面变得更有“记忆”。

欢迎点赞收藏,也期待你在评论区分享你的缓存策略或踩坑经历。

让图片学会“等你看到再出场”——懒加载全攻略

图片懒加载全解析:从传统 Scroll 到现代 IntersectionObserver

在前端开发的世界里,性能优化永远是绕不开的核心话题✨。尤其是在电商、资讯、社交这类图片密集型的页面中,大量图片的加载往往会成为页面性能的 “绊脚石”—— 首屏加载慢吞吞,用户没耐心直接离开;非可视区域的图片白白消耗带宽,服务器压力也徒增。

而图片懒加载(Lazy Load)作为前端性能优化的 “明星方案”,正是为解决这些痛点而生。今天我们就从概念、原理到实战,全方位拆解图片懒加载的实现逻辑,对比传统与现代方案的优劣,让你彻底吃透这个高频考点!

一、什么是图片懒加载?🤔

图片懒加载,本质是一种 “按需加载” 的资源加载策略:浏览器解析页面时,不会一次性加载所有<img>标签对应的图片,而是先加载首屏可视区域内的图片;当用户滚动页面,使原本隐藏在视口外的图片进入可视区域(Viewport)时,再触发这些图片的真实加载。

核心实现逻辑的关键是 “资源延迟绑定”:将图片的真实地址暂存到data-src(自定义属性)中,而非直接赋值给src属性(src先指向体积极小的占位图,如 1x1 透明图),只有满足 “进入视口” 条件时,才把data-src的值替换到src中,触发真实的图片 HTTP 请求。

二、为什么需要图片懒加载?💡

没有懒加载的页面,浏览器解析<img>标签时,只要看到src属性就会立刻发起请求,这会带来两个致命问题:

  1. 首屏加载速度慢:首页的所有图片请求会和 HTML、CSS、JS 的加载 “抢占” 网络资源,导致首屏 HTML 渲染、样式加载被阻塞,用户面对空白页面的等待时间变长(数据显示,首屏加载超过 3 秒,用户流失率超 50%)。
  2. 无效请求浪费:视口之外的图片(如下滚才能看到的列表项),加载后用户可能永远不会滚动到对应位置,既浪费了用户的移动带宽(尤其是移动端),也增加了服务器的并发压力。

而懒加载的引入,恰好解决了这些问题:

  1. ✅ 提升用户体验:首屏内容快速渲染,用户无需长时间等待;

  2. ✅ 节省带宽资源:仅加载用户能看到的图片,减少无效请求;

  3. ✅ 降低服务器压力:分散图片请求的时间和并发量,避免瞬间高并发。

三、图片懒加载的解决方案核心🔑

所有懒加载方案都围绕两个核心原则展开,缺一不可:

1. 首屏优先

暂时不需要加载的图片,src属性先指向小体积占位图(如 1x1 透明图、加载中占位图),让浏览器优先加载 HTML、CSS、JS 等核心资源,保证首屏内容快速呈现。

2. 按需加载

通过监听页面滚动(或原生 API 监听交集状态),实时判断图片是否进入视口;只有当图片进入视口时,才将data-src中的真实地址赋值给src,触发真实图片的加载。

四、如何实现图片懒加载?🛠️

接下来我们从代码层面,拆解传统方案和现代方案的实现逻辑,对比两者的优劣。

1. 传统方案:监听滚动事件(onscroll + 节流)

这是早期懒加载的主流实现方式,核心是 “监听滚动 + 节流控制 + 手动计算位置”。

1.1 核心思路

① 图片预处理:给非首屏图片添加lazy类,src赋值占位图,真实地址存在data-src自定义属性中;② 节流控制:给scroll事件绑定节流函数,避免高频触发导致性能卡顿;③ 视口判断:滚动时遍历所有lazy图片,通过getBoundingClientRect()计算图片与视口的位置关系,判断是否进入视口;④ 加载图片:若图片进入视口,将data-src赋值给src,移除lazy类、添加loaded类(用于样式过渡),并移除data-src属性;⑤ 初始化检查:页面加载完成后,先执行一次懒加载判断,避免首屏内的lazy图片未加载。

1.2 代码

javascript

// 节流函数:控制函数高频触发,避免滚动时性能卡顿
function throttle(func, wait) {
    let timeout = null; // 定时器标识,用于控制执行时机
    return function () {
        if (!timeout) { // 若定时器不存在,说明可以执行函数
            timeout = setTimeout(() => {
                func.apply(this, arguments); // 执行目标函数,保留this和参数
                timeout = null; // 执行完成后重置定时器
            }, wait);
        }
    };
}

function lazyLoad() {
    const lazyImages = document.querySelectorAll('img.lazy'); // 获取所有待加载的图片
    const windowHeight = window.innerHeight; // 获取视口高度

    lazyImages.forEach(img => {
        // 跳过已加载的图片(已移除lazy类)
        if (!img.classList.contains('lazy')) return;

        const rect = img.getBoundingClientRect(); // 获取图片的位置信息(相对于视口)
        // 核心判断:图片顶部进入视口下方,且底部未完全离开视口上方 → 图片进入视口
        if (rect.top < windowHeight && rect.bottom > 0) {
            if (img.dataset.src) {
                console.log('Loading image via Scroll:', img.dataset.src);
                img.src = img.dataset.src; // 替换src,触发真实图片加载
                img.removeAttribute('data-src'); // 移除自定义属性,避免重复加载
                img.classList.remove('lazy'); // 移除lazy类,标记为已加载
                img.classList.add('loaded'); // 添加loaded类,实现透明度过渡
            }
        }
    });
}

// 节流处理懒加载函数,200ms执行一次
const throttledLazyLoad = throttle(lazyLoad, 200);

// 监听滚动事件,触发节流后的懒加载
document.addEventListener('scroll', throttledLazyLoad);
// 窗口大小变化时,重新判断图片位置
window.addEventListener('resize', throttledLazyLoad);
// 页面加载完成后,初始化检查首屏图片
document.addEventListener('DOMContentLoaded', lazyLoad);
1.3 代码实现效果

观察界面滚动图片变化与控制台打印:

QQ20260201-145940.gif

1.4 该方案的缺点

❌ 性能损耗:即使加了节流,scroll事件仍会高频触发,存在一定的性能开销;❌ 代码冗余:需要手动计算元素与视口的位置关系,逻辑易出错,维护成本高;❌ 适配性差:在移动端、嵌套滚动等复杂布局中,位置计算容易失效,适配成本高。

1.5 关键 API 解析
  • throttle (func, wait):自定义节流函数,控制高频事件触发频率,避免性能卡顿。

    • func:需要被节流的目标函数(此处为 lazyLoad);
    • wait:节流等待时间(毫秒),此处为 200ms,即函数每 200ms 最多执行一次;
  • document.querySelectorAll ('img.lazy'):根据 CSS 选择器获取所有带 lazy 类的待加载图片,返回 NodeList 集合。

  • window.innerHeight:获取当前浏览器视口的高度,用于判断图片是否进入视口。

  • Element.classList.contains ('lazy'):布尔值,判断图片元素是否包含 lazy 类,跳过已加载的图片。

  • Element.getBoundingClientRect ():获取元素相对于视口的位置信息(返回 DOMRect 对象),包含 top(元素顶部距视口顶部距离)、bottom(元素底部距视口顶部距离)等属性。

  • img.removeAttribute ('data-src'):移除图片的 data-src 属性,避免重复读取。

  • Element.classList.remove ('lazy'):移除图片的 lazy 类,标记为已加载。

  • Element.classList.add ('loaded'):为图片添加 loaded 类,实现加载后的样式过渡。

  • document.addEventListener ('scroll', throttledLazyLoad):监听页面滚动事件,触发节流后的懒加载函数。

  • window.addEventListener ('resize', throttledLazyLoad):监听窗口大小变化事件,重新判断图片位置,适配视口尺寸变化。

  • document.addEventListener ('DOMContentLoaded', lazyLoad):监听 DOM 加载完成事件,初始化执行懒加载函数,检查首屏图片是否需要加载。

2. 现代方案:IntersectionObserver(推荐)🌟

为了解决传统方案的痛点,浏览器原生提供了IntersectionObserver API(交集观察器),专门用于监听 “元素是否进入视口 / 与其他元素产生交集”,是目前懒加载的最优解。

2.1 核心思路

① 浏览器原生支持:无需手动监听scrollresize等事件,由浏览器底层优化执行逻辑;② 交集监听:创建IntersectionObserver实例,指定观察的目标元素(lazy图片);③ 自动判断:当目标元素与视口产生交集(满足阈值条件)时,触发回调函数;④ 加载图片:在回调中替换data-srcsrc,移除lazy类,停止观察该元素(避免重复触发);⑤ 降级处理:若浏览器不支持该 API,直接加载所有图片,保证功能可用。

2.2 代码

javascript

document.addEventListener("DOMContentLoaded", function() {
    const lazyImages = document.querySelectorAll("img.lazy"); // 获取所有待加载图片

    // 检查浏览器是否支持IntersectionObserver
    if ("IntersectionObserver" in window) {
        // 创建交集观察器实例
        const imageObserver = new IntersectionObserver(function(entries, observer) {
            // 遍历所有被观察的元素的交集状态
            entries.forEach(function(entry) {
                // entry.isIntersecting:元素是否进入视口(产生交集)
                if (entry.isIntersecting) {
                    const img = entry.target; // 获取当前触发的图片元素
                    console.log('Loading image via IntersectionObserver:', img.dataset.src);
                    img.src = img.dataset.src; // 替换src,加载真实图片
                    img.classList.remove("lazy"); // 标记为已加载
                    img.classList.add("loaded"); // 添加样式过渡类
                    observer.unobserve(img); // 停止观察该图片,避免重复触发
                }
            });
        }, {
            root: null, // 观察的根元素:null表示视口
            rootMargin: "0px", // 根元素的边距,扩展/缩小观察区域
            threshold: 0.1 // 阈值:图片10%可见时触发回调
        });

        // 遍历所有lazy图片,开始观察
        lazyImages.forEach(function(image) {
            imageObserver.observe(image);
        });
    } else {
        // 降级处理:不支持时直接加载所有图片
        console.log("IntersectionObserver not supported, loading all images.");
        lazyImages.forEach(function(img) {
            img.src = img.dataset.src;
            img.classList.remove("lazy");
            img.classList.add("loaded");
        });
    }
});
2.3 代码实现效果

观察界面滚动图片变化与控制台打印:

QQ20260201-151226.gif

2.4 该方案的优势

✅ 无性能损耗:浏览器底层实现,无需手动节流 / 防抖,性能远超scroll监听;✅ 代码简洁:无需手动计算元素位置,逻辑清晰,维护成本低;✅ 适配性强:完美兼容移动端、嵌套滚动等复杂布局;✅ 可扩展:支持自定义观察规则(如rootMargin扩展观察区域、threshold调整触发阈值)。

2.5 关键 API 解析
  • IntersectionObserver(callback, options):构造函数,创建交集观察器实例。

    • callback:交集状态变化时的回调函数,接收两个参数:

      • entries:数组,每个元素是IntersectionObserverEntry对象,包含元素的交集状态、位置等信息;
      • observer:当前的IntersectionObserver实例。
    • options:配置项(可选):

      • root:观察的根元素,默认null(视口);
      • rootMargin:根元素的边距,格式同 CSS margin(如 "100px 0"),可扩展 / 缩小观察区域;
      • threshold:触发回调的阈值(0~1),0 表示元素刚进入视口就触发,1 表示元素完全进入视口才触发。
  • entry.isIntersecting:布尔值,判断元素是否与根元素产生交集(进入视口)。

  • observer.observe(target):开始观察指定的目标元素。

  • observer.unobserve(target):停止观察指定的目标元素。

五、CSS 与 HTML 代码

CSS:

<style>
        /* 页面基础样式 */
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            text-align: center;
        }

        /* 
         * 空白间隔区样式
         * 用于撑开页面高度,模拟长页面滚动效果
         */
        .spacer {
            height: 150vh;
            /* 核心:高度设置为 1.5 倍视口高度 (150vh) */
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-start;
            /* 内容从顶部开始排列 */
            padding-top: 50vh;
            /* 核心:提示词距离顶部 1/3 (50vh / 150vh ≈ 0.33) */
            box-sizing: border-box;
            background-color: #f9f9f9;
            border-bottom: 1px solid #ddd;
        }

        /* 图片容器样式 */
        .image-wrapper {
            padding: 50px 0;
            background-color: #fff;
            min-height: 400px;
            /* 最小高度,防止图片加载前高度塌陷 */
            display: flex;
            align-items: center;
            justify-content: center;
        }

        /* 
         * 懒加载图片样式
         * .lazy 类表示图片尚未加载
         */
        img.lazy {
            max-width: 80%;
            height: auto;
            display: block;
            margin: 0 auto;
            opacity: 0.3;
            /* 初始低透明度,显示占位效果 */
            transition: opacity 0.5s;
            /* 添加过渡效果,使加载更平滑 */
        }

        /* 
         * 图片加载完成后的样式
         * .loaded 类在 JS 中加载完成后添加
         */
        img.loaded {
            opacity: 1;
            /* 恢复完全不透明 */
        }

        h1,
        h2 {
            color: #333;
        }
    </style>

HTML

<body>
    <!-- 
      第一部分:首屏空白区
      作用:展示标题和提示,迫使用户向下滚动
    -->
    <div class="spacer">
        <h1>传统懒加载方案</h1>
        <h2>⬇️ 向下滑动加载第一张图片 ⬇️</h2>
    </div>

    <!-- 
      第二部分:第一张图片
      data-src 存储真实图片地址,src 存储占位图
    -->
    <div class="image-wrapper">
        <img class="lazy"
            src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png"
            data-src="https://img.36krcdn.com/hsossms/20260119/v2_53cad3f2226f48e2afc1942de3ab74e4@5888275@ai_oswg1141728oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp"
            alt="Image 1">
    </div>

    <!-- 
      第三部分:中间间隔区
      作用:分隔两张图片,确保加载第二张图片需要继续大幅滚动
    -->
    <div class="spacer">
        <h2>⬇️ 向下滑动出现第二张图片 ⬇️</h2>
    </div>

    <!-- 第四部分:第二张图片 -->
    <div class="image-wrapper">
        <img class="lazy"
            src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png"
            data-src="https://img.36krcdn.com/hsossms/20260117/v2_1e74add07bb94971845c777e0ce87a49@000000@ai_oswg421938oswg1536oswg722_img_000~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp"
            alt="Image 2">
    </div>

    <!-- 底部留白,确保能滚到底部,方便观察最后一张图的加载 -->
    <div style="height: 50vh; background-color: #f9f9f9;"></div>
</body>

六、面试官会问🤨

  1. 图片懒加载的核心原理是什么?

答:核心是 “按需加载”,将非首屏图片的真实地址存到data-src(自定义属性),src指向占位图;通过监听滚动(传统)或IntersectionObserver(现代)判断图片是否进入视口,进入后将data-src赋值给src,触发真实图片加载。

  1. 传统懒加载方案中,为什么要使用节流函数?

答:scroll事件会在滚动过程中高频触发(每秒数十次),若直接执行懒加载逻辑,会导致大量 DOM 操作和计算,引发页面卡顿;节流函数能控制函数在指定时间内只执行一次,减少性能损耗。

  1. IntersectionObserver 相比传统 scroll 方案有哪些优势?

答:① 性能更好:浏览器底层优化,无需手动节流;② 代码更简洁:无需手动计算元素位置;③ 适配性强:兼容复杂布局;④ 可扩展:支持自定义观察规则。

  1. 如何判断一个元素是否进入视口?

答:传统方案用element.getBoundingClientRect()获取元素的位置信息,判断rect.top < window.innerHeight && rect.bottom > 0;现代方案直接通过IntersectionObserverisIntersecting属性判断。

  1. 懒加载的降级方案是什么?

答:若浏览器不支持IntersectionObserver(如部分老旧浏览器),直接遍历所有lazy图片,将data-src赋值给src,保证图片能正常加载。

七、结语🎯

图片懒加载作为前端性能优化的 “基础操作”,其核心始终是 “按需加载”—— 优先保证首屏体验,减少无效资源消耗。传统的scroll+节流方案兼容旧浏览器,但存在性能和适配痛点;而IntersectionObserver作为现代方案,凭借浏览器原生优化、简洁的代码逻辑,成为当前懒加载的首选。

在实际开发中,我们需要根据项目的兼容需求选择方案:若需兼容老旧浏览器,可采用 “IntersectionObserver+scroll 降级” 的混合方案;若面向现代浏览器,直接使用IntersectionObserver即可。

性能优化没有银弹,但图片懒加载是列表类页面(电商、资讯、社交)的 “必做优化”,小小的改动就能显著提升页面加载速度和用户体验。希望这篇文章能帮你彻底吃透图片懒加载,无论是面试还是实战,都能游刃有余!

❌