跳到主要内容

懒执行与懒加载

面试速答(30 秒版 TL;DR)

  • 懒执行(Lazy Execution) 关注的是:代码什么时候算、什么时候跑。
  • 懒加载(Lazy Loading) 关注的是:资源什么时候下载、什么时候进入页面。
  • 两者经常一起出现,但不是一回事:
    • 懒加载解决“先别下载”
    • 懒执行解决“先别计算、先别初始化”
  • 典型例子:
    • 路由组件 import():既可能触发懒加载,也可能把模块执行推迟到真正进入路由时
    • 图片 loading="lazy":是懒加载,不是懒执行
    • Vue computed:典型懒执行,不涉及资源加载

一、为什么这一题很容易混?

因为很多优化动作表面上都叫“lazy”,但它们作用在不同层面:

  • 有的是 不下载
  • 有的是 下载了但先不执行
  • 有的是 执行结果先不计算
  • 有的是 等用户看到、点击、滚动到再触发

如果面试时不先分层,很容易把很多不同机制混成一句“按需加载”。

更稳的说法是:

懒加载管“资源进入系统的时机”,懒执行管“逻辑真正运行的时机”。


二、先区分:资源层 vs 执行层

维度懒加载懒执行
核心问题资源何时下载逻辑何时运行
优化对象图片、脚本、模块、组件、iframe计算、初始化、副作用、监听器、渲染逻辑
常见手段loading="lazy"、动态 import()、路由分包惰性求值、按事件触发、按可见性触发、延迟初始化
目标降低首屏请求与传输压力降低首屏主线程压力与无效计算

这张表最好能记住,因为它能把回答立刻拉开层次。


三、懒加载到底在优化什么?

懒加载的本质是:

当前不用的资源,不要在首屏阶段就把它们都拉下来。

典型对象包括:

  • 首屏以下图片
  • 路由级页面代码
  • 很重的图表库、编辑器、地图 SDK
  • 评论区、推荐区、客服脚本
  • 视口外 iframe

示例 1:图片懒加载

<img src="/images/card.webp" loading="lazy" alt="商品图" width="320" height="240" />

示例 2:路由组件懒加载

const UserPage = () => import('./pages/UserPage.js')

懒加载的直接收益通常是:

  • 首屏请求数更少
  • 首屏下载体积更小
  • 当前页面关键资源竞争更少

四、懒执行又在优化什么?

懒执行的本质是:

当前不需要的逻辑,先别跑;等真正有人用到时再运行。

典型场景:

  • 大对象或大列表的派生计算
  • 弹窗、抽屉、富文本编辑器的初始化
  • 图表首次渲染
  • 非关键埋点、监控、推荐算法
  • 只有用户交互后才需要的副作用逻辑

一个最小例子:

let panelInstance

function openPanel() {
if (!panelInstance) {
panelInstance = createHeavyPanel()
}
panelInstance.show()
}

这里没有网络请求优化,但明显减少了初始执行成本。


五、两者经常如何配合?

很多真实优化并不是二选一,而是一起用。

比如一个图表面板:

  1. 首屏不下载图表库代码
  2. 用户点开“数据分析”标签页时,再动态 import 图表模块
  3. 模块下载后,真正渲染图表也等容器可见时再初始化

这就是:

  • 第一层:懒加载
  • 第二层:懒执行

先记两层决策:要不要先下载,下载后要不要立刻执行。

这张图要表达的重点是:

先决定下不下载,再决定下完后是不是立刻执行。


六、典型手段有哪些?

1. 图片、iframe 原生懒加载

<img src="/images/list-item.webp" loading="lazy" alt="列表图片" />
<iframe src="https://example.com/embed" loading="lazy" title="第三方内容"></iframe>

2. 动态 import

button.addEventListener('click', async () => {
const { mountEditor } = await import('./editor.js')
mountEditor()
})

这段代码里同时包含两层含义:

  • editor.js 是按需下载
  • mountEditor() 是按点击时机执行

3. 视口可见时再启动逻辑

const observer = new IntersectionObserver(async ([entry]) => {
if (!entry.isIntersecting) return

const { mountChart } = await import('./chart.js')
mountChart(entry.target)
observer.disconnect()
})

observer.observe(document.querySelector('#chart'))

这是很典型的“懒加载 + 懒执行”组合拳。


七、它们分别会踩什么坑?

懒加载常见坑

  • 首屏关键资源也被懒加载,反而拖慢 LCP
  • 图片懒加载但没有预留尺寸,导致 CLS
  • 包拆太碎,请求调度变复杂
  • 路由切换时才发现 chunk 很大,切页卡顿

懒执行常见坑

  • 首次交互时集中初始化,导致点击后卡顿
  • 逻辑延后后,状态依赖没理顺,产生时序问题
  • 过度推迟埋点或监听,导致丢数据
  • 某些逻辑每次触发都重新初始化,反而重复开销更大

面试里如果你能补这一层“优化也会转移成本”,答案会更成熟。


八、怎么判断该不该用?

可以用一个很实用的判断顺序:

  1. 这个资源或逻辑是不是首屏必需?
  2. 如果现在不下载/不执行,用户会不会立刻感知异常?
  3. 延后之后,是否会把成本堆到首次交互时爆发?
  4. 是否有明确触发条件,比如点击、滚动、进入视口、切换路由?

如果一个功能是首屏主内容、关键交互、LCP 元素,那通常不适合盲目懒处理。


九、典型题与标准答法

Q1:懒执行和懒加载有什么区别?

答法:

懒加载解决的是资源什么时候下载,懒执行解决的是逻辑什么时候运行。一个偏资源调度,一个偏运行时计算。它们经常一起使用,但不是同一个概念。

Q2:动态 import 算懒加载还是懒执行?

答法:

首先它一定包含懒加载,因为模块不是首屏就下载;其次它通常也会把模块执行推迟到真正触发 import 的时机。所以很多场景里它同时体现了两者。

Q3:是不是所有非首屏内容都该懒处理?

答法:

不是。要看它是否会拖慢关键路径,以及延后后会不会把压力堆到用户首次交互。优化不能只看“首屏更轻了”,还要看“后续会不会更卡”。

Q4:懒加载一定能提升性能吗?

答法:

不一定。它通常能减轻首屏负担,但也可能带来切页等待、首次点击卡顿、请求碎片化等副作用。关键是把资源和逻辑延后到合适的时机,而不是机械地全部按需。


十、常见误区

  • 误区 1:把所有 lazy 都叫按需加载。 最好分清资源和执行两个层面。

  • 误区 2:首屏关键图也开 loading="lazy" 这会伤害 LCP。

  • 误区 3:只考虑首屏,不考虑首次交互。 成本只是被转移了,不一定被消灭了。

  • 误区 4:拆包越细越好。 包太碎会带来新的调度成本。


十一、速记要点

  • 懒加载:先别下载。
  • 懒执行:先别运行。
  • 前者优化请求和传输,后者优化主线程和计算。
  • 动态 import 常常同时具备两者特征。
  • 优化不是无脑延后,而是把工作放到真正需要的时机。