Skip to content

华为OD前端技术面试指南

一、基础知识(10题)

1. 盒模型及其应用场景

面试官可能怎么问:

  • "请讲一下CSS盒模型"
  • "box-sizing的两个值有什么区别?什么时候用哪个?"
  • "你项目中遇到过盒模型相关的问题吗?"

推荐回答框架:

CSS盒模型定义了元素在页面中占据空间的计算方式,分为两种:

  1. 标准盒模型(content-box):width/height只包含content,padding和border额外计算
  2. 怪异盒模型(border-box):width/height包含content + padding + border

应用场景结合你的项目:

  • 在智慧工地安全监控系统中,大屏仪表板布局时,使用border-box可以更方便地控制元素总尺寸
  • 在华为WeLink的IM消息气泡中,使用border-box确保padding增加时整体宽度不变
css
/* 全局重置,统一盒模型计算方式 */
*, *::before, *::after {
  box-sizing: border-box;
}

/* 实际项目示例:消息气泡 */
.message-bubble {
  box-sizing: border-box;
  width: 300px;        /* 总宽度固定为300px */
  padding: 12px 16px;  /* padding在内部消化 */
  border: 1px solid #e0e0e0;
  /* 最终content宽度 = 300 - 32(padding) - 2(border) = 266px */
}

/* 大屏仪表板卡片 */
.dashboard-card {
  box-sizing: border-box;
  width: 25%;          /* 响应式布局,4列 */
  padding: 20px;
  border: 1px solid rgba(255,255,255,0.1);
  /* 不用担心padding撑破布局 */
}

延伸考点: margin合并、BFC(块级格式化上下文)


2. Flex和Grid布局的区别

面试官可能怎么问:

  • "什么时候用Flex,什么时候用Grid?"
  • "Grid的auto-fill和auto-fit有什么区别?"
  • "Flex的align-items和justify-content分别控制什么?"

推荐回答框架:

维度FlexGrid
维度一维布局(行或列)二维布局(行和列同时)
适用场景组件内部排列、导航栏、居中整体页面布局、复杂网格
对齐控制单轴对齐双轴对齐
间隙控制gap(较新)gap(原生支持)

结合项目回答:

css
/* Flex:WeLink消息列表的头部工具栏 */
.chat-header {
  display: flex;
  justify-content: space-between;  /* 两端对齐 */
  align-items: center;              /* 垂直居中 */
  padding: 0 16px;
  height: 56px;
}

/* Grid:智慧工地大屏仪表板 */
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);  /* 4列等宽 */
  grid-template-rows: auto auto;           /* 2行自适应 */
  gap: 20px;
}

/* Grid:更复杂的区域布局 */
.dashboard-complex {
  display: grid;
  grid-template-areas:
    "header header header"
    "sidebar main main"
    "footer footer footer";
  grid-template-columns: 200px 1fr 1fr;
  grid-template-rows: 60px 1fr 40px;
  gap: 10px;
}

关键区别代码示例:

css
/* auto-fill vs auto-fit - 常考点 */
.grid-fill {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  /* 空间不够时,保留空轨道(可能右侧留白) */
}

.grid-fit {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  /* 空间不够时,折叠空轨道(拉伸填充) */
}

flex: 1 是 CSS 布局中含金量极高的考点,也是面试官最喜欢用来“挖坑”的属性。

很多人只知道它是“自适应占满”,但面试官考察的是拆解能力(到底哪几个属性的组合)计算逻辑(剩余空间怎么分配)


第一层:基础认知(面试第一问:flex: 1 到底是什么?)
  • 标准回答flex: 1flex-growflex-shrinkflex-basis 这三个属性的简写
  • 完整展开flex: 1 等同于:
    css
    flex-grow: 1;    /* 有剩余空间时,我要分一份(按比例放大) */
    flex-shrink: 1;  /* 空间不足时,我要被压缩一份(按比例缩小) */
    flex-basis: 0%;  /* 在分配空间之前,我的初始基准尺寸是 0(完全忽略内容宽度) */

第二层:进阶深度(灵魂三问:1、auto、none、0 的区别)

面试官不会只问定义,他会甩出一组对比让你辨析。这是二面最高频的“组合拳”:

属性值对应拆解核心表现(面试必背)
flex: 11 1 0%完全弹性:忽略内容宽度,所有子项按比例平分容器剩余空间。常用于等分布局(如网格卡片)
flex: auto1 1 auto基于内容弹性:先根据内容大小占据空间,把剩余空间按比例分配。如果内容很大,它占的份额就大。
flex: none0 0 auto固定尺寸:既不放大的,也不缩小,完全由内容或具体 width 决定。常用于侧边栏或固定按钮。
flex: 00 1 0%最小收缩:初始尺寸为 0,不主动索取剩余空间,但空间不足时允许被压缩。很少用。

面试官追问:“flex: 1flex: auto 在宽度表现上最核心的区别是什么?”

你的高分回答(举例说明)

“假设父容器宽 600px,里面有两个子项,内容宽度都是 100px。

  • 如果用 flex: 1(basis: 0%):它会忽略内容的 100px,把 600px 当作纯剩余空间平分,两个子项各占 300px
  • 如果用 flex: auto(basis: auto):它会先扣除内容的 200px(100+100),再把剩下的 400px 按比例分配,每个子项最终是 100 + 200 = 300px。如果内容宽度不同(比如 100px 和 200px),flex:auto 会让大内容项占据更多空间,而 flex:1 依然是绝对平分。”

第三层:计算法则(面试手撕场景:剩余空间怎么分?)

面试官给一道场景题:“父容器宽 900px,三个子项 A、B、C 分别设置 flex: 1flex: 2flex: 3,问各自宽度是多少?”

你的解题步骤(展现逻辑推导)

  1. 首先看 flex-basis:都是 0%,所以初始尺寸都是 0
  2. 剩余空间 = 父容器总宽(900px) - 子项初始尺寸总和(0 + 0 + 0) = 900px。
  3. flex-grow 比例分配:总份数 = 1 + 2 + 3 = 6。
  4. 最终宽度:
    • A:0 + (1/6) * 900 = 150px
    • B:0 + (2/6) * 900 = 300px
    • C:0 + (3/6) * 900 = 450px

进阶加码(如果内容有 min-width 怎么办?)

“如果其中某个子项有 min-width: 300px,那它不会压缩到 150px。计算时,浏览器会先扣除这个强制最小宽度,剩下的空间再按比例分配。这也是为什么有时候 flex:1 撑不开或缩不小,往往是被 min-width 或内容撑死了。”


第四层:实战避坑(结合你的简历业务场景)

面试官问:“你在实际项目中使用 flex:1 遇到过什么坑?怎么解决的?”

坑点一:内容溢出导致布局被撑破(最经典)

(结合 IRMP 报告列表页) “在 IRMP 平台,我用 flex:1 做左侧树形菜单(200px)和右侧内容区(自适应)。结果右侧内容区里放了一个很长的英文链接(如 http://verylong...),因为没有空格换行,直接把右侧容器撑破了,左侧菜单也被挤变形。”

解决方案

  1. 根源修复:在 flex:1 的容器上加 min-width: 0(因为 Flex 子项默认 min-width: auto,防止溢出)。
  2. 文字截断兜底:配合 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; 强制换行或截断。
css
.flex-item {
  flex: 1;
  min-width: 0; /* 关键:允许容器收缩到比内容更小 */
  word-break: break-all; /* 强制长单词换行 */
}

坑点二:flex:1 在响应式中的滥用

(结合 MMMP 大屏仪表板) “在 MMMP 大屏仪表板中,我希望三个图表卡片等宽排列。如果用 flex:1,在 1920px 屏幕上很完美,但缩小到 1366px 时,因为卡片内有 min-height 或固定内边距,flex:1 虽然缩小了宽度,但高度没变,导致内部图表 Canvas 变形。”

解决方案:不再单纯依赖 flex:1,而是配合 aspect-ratio(宽高比)锁定卡片比例,或者改用 grid 布局 + minmax(200px, 1fr) 做更可控的响应式。


第五层:面试官“变态追问”与你的终极回应

追问 1:“flex: 1flex-grow: 1; width: 100%; 有什么区别?”

width: 100% 是相对于父容器的内容区(content-box),如果父容器有 padding,子项可能溢出。而 flex: 1flex-basis: 0% 是相对于弹性容器可用空间,它会自动扣除父容器的 paddinggap计算更精准,且不会溢出。”

追问 2:“如果父容器设置了 flex-direction: column(纵向),flex: 1 还管用吗?”

“当然管用!在纵轴(高度)方向上同样生效。此时 flex-basis: 0% 指的是高度为 0flex-grow 按比例分配的是父容器的剩余高度。这在做后台系统‘头部固定,内容区自动撑满剩余高度’时极其常用。”



3. JavaScript闭包是什么,有什么用途?

面试官可能怎么问:

  • "什么是闭包?"
  • "闭包会导致内存泄漏吗?"
  • "你在项目中用过闭包吗?举个例子"
  • "循环中使用闭包的常见问题怎么解决?"

推荐回答框架:

闭包是指函数能够记住并访问其词法作用域,即使该函数在当前词法作用域之外执行

核心三要素:

  1. 函数嵌套函数
  2. 内部函数引用外部函数的变量
  3. 内部函数被返回到外部使用

结合项目回答(WeLink消息模块):

javascript
// 场景1:消息模块的防抖搜索(结合手撕代码题)
function createDebouncedSearch() {
  let timer = null;  // 闭包保存timer状态

  return function(keyword) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      console.log('搜索:', keyword);
      // 实际调用API搜索消息
    }, 300);
  };
}

const searchMessages = createDebouncedSearch();
searchMessages('华为');  // 300ms后执行
searchMessages('华为OD'); // 重新计时

// 场景2:WeLink消息列表的私有状态管理
function createMessageStore() {
  let messages = [];      // 私有变量,外部无法直接访问
  let unreadCount = 0;

  return {
    addMessage(msg) {
      messages.push(msg);
      if (!msg.read) unreadCount++;
    },
    getMessages() {
      return [...messages]; // 返回副本,防止外部修改
    },
    getUnreadCount() {
      return unreadCount;
    },
    markAsRead(id) {
      const msg = messages.find(m => m.id === id);
      if (msg && !msg.read) {
        msg.read = true;
        unreadCount--;
      }
    }
  };
}

const store = createMessageStore();
// messages和unreadCount被闭包保护,无法从外部直接修改

经典循环闭包问题:

javascript
// ❌ 错误:循环中的var
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 输出 3, 3, 3
}

// ✅ 方案1:使用IIFE(立即执行函数)
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100); // 输出 0, 1, 2
  })(i);
}

// ✅ 方案2:使用let(ES6块级作用域)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 输出 0, 1, 2
}

内存泄漏说明: 闭包本身不会导致内存泄漏,但如果闭包持有大量数据且长期不被释放,会造成内存占用过高。


4. 事件循环机制怎么理解?

面试官可能怎么问:

  • "讲一下事件循环(Event Loop)"
  • "宏任务和微任务的区别?执行顺序?"
  • "setTimeout(fn, 0)什么时候执行?"
  • "Promise.then和setTimeout哪个先执行?"
  • "async/await的执行顺序?"

推荐回答框架:

JavaScript是单线程语言,事件循环是其异步编程的核心机制,负责协调调用栈、宏任务队列和微任务队列的执行。

执行顺序(重点):

  1. 同步代码 → 调用栈
  2. 微任务(Promise.then/catch/finally、MutationObserver、queueMicrotask)
  3. 宏任务(setTimeout、setInterval、setImmediate、I/O、UI渲染)
  4. 重复步骤2-3

结合项目(WebSocket实时告警推送):

javascript
console.log('1. 同步代码开始');

setTimeout(() => {
  console.log('5. setTimeout宏任务');
}, 0);

Promise.resolve().then(() => {
  console.log('3. Promise微任务1');
});

Promise.resolve().then(() => {
  console.log('4. Promise微任务2');
});

console.log('2. 同步代码结束');

// 输出顺序:1 → 2 → 3 → 4 → 5

// ========== 实际项目:WebSocket消息处理 ==========
class AlertService {
  constructor() {
    this.ws = new WebSocket('wss://alert-server.com');
    this.messageQueue = []; // 消息队列

    this.ws.onmessage = (event) => {
      const alert = JSON.parse(event.data);

      // 微任务:立即处理高优先级告警(不阻塞渲染)
      Promise.resolve().then(() => {
        if (alert.level === 'critical') {
          this.showCriticalAlert(alert); // 立即弹窗
        }
      });

      // 宏任务:延迟处理普通告警
      setTimeout(() => {
        if (alert.level === 'normal') {
          this.showNotification(alert); // 延迟通知
        }
      }, 100);
    };
  }
}

async/await执行顺序(高频考点):

javascript
async function test() {
  console.log('1. async开始');

  await Promise.resolve();  // await后面的代码变成微任务
  console.log('3. await之后');

  await new Promise(resolve => {
    console.log('4. Promise构造器同步执行');
    resolve();
  });
  console.log('5. 第二个await之后');
}

test();
console.log('2. 同步代码');

// 输出:1 → 2 → 3 → 4 → 5

综合面试题(必会):

javascript
console.log('1');

setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => console.log('3'));
}, 0);

Promise.resolve().then(() => {
  console.log('4');
  setTimeout(() => console.log('5'), 0);
});

console.log('6');

// 输出:1 → 6 → 4 → 2 → 3 → 5
// 第一轮:同步 1, 6 → 微任务 4 → 宏任务 2
// 第二轮:微任务 3 → 宏任务 5

5. Promise有几种状态,如何链式调用?

面试官可能怎么问:

  • "Promise有几种状态?"
  • "Promise.all和Promise.race的区别?"
  • "Promise.allSettled是干什么的?"
  • "如何实现一个Promise?"
  • "Promise链中错误怎么捕获?"

推荐回答框架:

Promise三种状态:

  • pending:进行中
  • fulfilled:已成功
  • rejected:已失败

状态转换:pending → fulfilled 或 pending → rejected,不可逆

链式调用核心: then返回一个新的Promise

javascript
// ========== 基础链式调用 ==========
fetch('/api/user')
  .then(res => res.json())      // 返回解析后的数据
  .then(data => fetch(`/api/orders/${data.id}`))  // 返回新的Promise
  .then(res => res.json())
  .then(orders => console.log(orders))
  .catch(err => console.error('请求失败:', err))
  .finally(() => console.log('请求完成'));

// ========== 结合项目:IRMP报告平台的多步骤流程 ==========
class ReportService {
  async generateReport(templateId, data) {
    return fetch(`/api/templates/${templateId}`)
      .then(res => {
        if (!res.ok) throw new Error('模板获取失败');
        return res.json();
      })
      .then(template => this.fillTemplate(template, data))
      .then(filled => this.validateReport(filled))
      .then(valid => {
        if (!valid) throw new Error('报告验证失败');
        return this.renderPDF(valid);
      })
      .then(pdf => this.saveReport(pdf))
      .catch(err => {
        console.error('报告生成失败:', err);
        throw err; // 继续向上抛,让调用方处理
      });
  }
}

// ========== Promise.all:并行请求(智慧工地多设备数据) ==========
async function fetchDashboardData() {
  const [devices, alerts, envData] = await Promise.all([
    fetch('/api/devices').then(r => r.json()),
    fetch('/api/alerts').then(r => r.json()),
    fetch('/api/environment').then(r => r.json())
  ]);
  return { devices, alerts, envData };
}

// ========== Promise.race:超时控制(WebRTC连接) ==========
function connectWithTimeout(peerConnection, timeout = 5000) {
  return Promise.race([
    new Promise((resolve, reject) => {
      peerConnection.oniceconnectionstatechange = () => {
        if (peerConnection.iceConnectionState === 'connected') {
          resolve('连接成功');
        }
      };
    }),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('连接超时')), timeout)
    )
  ]);
}

// ========== Promise.allSettled:批量操作不中断 ==========
async function batchApproveReports(reportIds) {
  const results = await Promise.allSettled(
    reportIds.map(id => fetch(`/api/reports/${id}/approve`, { method: 'POST' }))
  );

  const success = results.filter(r => r.status === 'fulfilled');
  const failed = results.filter(r => r.status === 'rejected');

  return { success, failed };
}

// ========== 手写Promise(简化版) ==========
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn(value));
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn(reason));
      }
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        try {
          const result = onFulfilled(this.value);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      } else if (this.state === 'rejected') {
        try {
          const result = onRejected(this.reason);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      } else {
        this.onFulfilledCallbacks.push((value) => {
          try {
            const result = onFulfilled(value);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
        this.onRejectedCallbacks.push((reason) => {
          try {
            const result = onRejected(reason);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
      }
    });
  }
}

前端请求超时处理是Promise 综合应用必考实战题,面试官不仅要看你知不知道 Promise.race,还要看你**有没有真实处理过「请求超时后中断请求并清理资源」**的工程经验。


第一层:基础版 —— 用 Promise.race 实现超时控制
  • 核心思想Promise.race 接受多个 Promise,谁先完成(或拒绝),race 就返回谁的结果。我们把「真实请求」和「超时定时器」放在一起赛跑。

手写基础代码(面试必会)

javascript
function requestWithTimeout(fetchPromise, timeout = 5000) {
  // 1. 创建一个超时拒绝的 Promise
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`请求超时(${timeout}ms)`));
    }, timeout);
  });

  // 2. 用 race 让真实请求和超时 Promise 赛跑
  return Promise.race([fetchPromise, timeoutPromise]);
}

// 使用示例
const fetchData = fetch('/api/users'); // 假设这是一个 fetch 请求
requestWithTimeout(fetchData, 3000)
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err)); // 超时或请求失败都会进这里

第二层:进阶版 —— 超时后主动中断请求(AbortController)

问题:上面的基础版有致命缺陷——超时后定时器触发了 reject,但真实的 fetch 请求依然在后台进行,没有被中断,会浪费带宽和服务器资源。

解决方案:使用 AbortController 主动中断请求(这也是现代浏览器和 Node.js 的标准做法)。

面试必背完整版代码

javascript
function requestWithAbort(url, options = {}, timeout = 5000) {
  // 1. 创建 AbortController 实例
  const controller = new AbortController();
  const { signal } = controller;

  // 2. 将 signal 传入 fetch 的 options 中
  const fetchPromise = fetch(url, { ...options, signal });

  // 3. 创建超时定时器:超时后调用 controller.abort()
  const timeoutId = setTimeout(() => {
    controller.abort(); // 主动中断请求
  }, timeout);

  // 4. 返回一个 Promise:如果请求完成/失败,清除定时器
  return fetchPromise
    .then(response => {
      clearTimeout(timeoutId);
      return response;
    })
    .catch(err => {
      clearTimeout(timeoutId);
      // 判断是否由 abort 触发
      if (err.name === 'AbortError') {
        throw new Error(`请求超时(${timeout}ms)`);
      }
      throw err; // 其他错误正常抛出
    });
}

// 使用示例
requestWithAbort('/api/report/generate', { method: 'POST' }, 30000)
  .then(res => res.json())
  .then(data => console.log('报告生成成功', data))
  .catch(err => {
    if (err.message.includes('超时')) {
      console.log('生成耗时太长,请稍后查看');
    }
  });

面试官追问:“AbortController 除了 fetch,还能用在什么地方?”

你的回答:“AbortController 是一个通用中断机制,不仅用于 fetch,还可以配合 addEventListener 移除事件监听、中断 axios 请求(axios 原生支持 cancelToken,新版也支持 signal)、甚至用来取消 async 函数中的某些耗时操作(配合 signal.addEventListener('abort', ...))。”


第三层:实战封装 —— 结合 Axios 拦截器(你简历里一定用过)

面试官追问:“你在项目里是用 axios 还是原生 fetch?如果让你封装一个带超时和错误重试的请求工具,你怎么写?”

你的回答(结合 IRMP 报告生成平台)

“在 IRMP 项目中,我用 axios 封装了统一的请求实例,配置了默认超时、请求/响应拦截器,以及超时自动重试(仅限幂等请求) 的逻辑。”

javascript
// axios 超时配置(最基础的)
const instance = axios.create({
  timeout: 10000, // 全局超时 10 秒
});

// 【进阶】带重试机制的封装(面试加分项)
function requestWithRetry(config, retries = 2) {
  return new Promise((resolve, reject) => {
    const attempt = (retryLeft) => {
      instance(config)
        .then(resolve)
        .catch(err => {
          // 如果是超时错误(code: 'ECONNABORTED')且有重试次数,就重试
          if (err.code === 'ECONNABORTED' && retryLeft > 0) {
            console.log(`请求超时,剩余重试次数: ${retryLeft}`);
            attempt(retryLeft - 1);
          } else {
            reject(err);
          }
        });
    };
    attempt(retries);
  });
}

// 使用场景:IRMP 报告生成(PDF 生成耗时较长,可能偶发超时)
requestWithRetry({
  url: '/api/report/generate',
  method: 'POST',
  data: { taskId: '123' },
  timeout: 30000, // 单次超时 30 秒
}, 2) // 超时后最多重试 2 次
  .then(res => console.log('报告生成完成'))
  .catch(err => console.error('生成失败', err));

面试官追问:“什么接口可以重试?什么接口不能重试?”

你的回答:“只有幂等的 GET 请求可以重试(如查询列表、查询详情)。像 IRMP 的 /api/report/generate(生成报告)是 POST 非幂等请求绝对不能自动重试,否则会导致生成两份重复报告。对于非幂等请求,我的策略是:超时后直接返回友好提示,让用户手动点击‘重新生成’,同时后端配合幂等键防止重复提交。”


第四层:结合你的简历(三个项目的话术)
场景一:IRMP 报告生成(长耗时请求)

“报告生成接口有时需要 20-30 秒,浏览器默认超时是 60 秒,但 Nginx 反向代理可能 10 秒就断开了。我做了两层超时控制:

  1. 前端超时:axios 配置 timeout: 60000,配合 AbortController 在超时时主动中断请求。
  2. 兜底策略:超时后不是直接报错,而是轮询查询任务状态(用 taskId 去问后端‘生成好了没’),给用户展示进度条,而不是直接抛给用户一个‘网络错误’。”
场景二:MMMP 设备告警(长连接心跳超时)

“在 MMMP 平台,WebSocket 连接可能因为网络波动静默断开。我封装了心跳机制(Heartbeat):每隔 30 秒发送一次 ping,如果 3 秒内没有收到 pong,就判定超时,主动 ws.close() 并触发指数退避重连(1s、2s、4s...,最大延迟 30s)。”

javascript
class WebSocketHeartbeat {
  constructor(url) {
    this.ws = new WebSocket(url);
    this.pingInterval = null;
    this.pongTimeout = null;
    this.init();
  }

  init() {
    this.ws.onopen = () => {
      this.startHeartbeat();
    };
    this.ws.onmessage = (e) => {
      if (e.data === 'pong') {
        clearTimeout(this.pongTimeout); // 收到 pong,清除超时定时器
        this.pongTimeout = null;
      }
    };
  }

  startHeartbeat() {
    this.pingInterval = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send('ping');
        // 设置 pong 超时:如果 3 秒内没收到 pong,判定连接假死
        this.pongTimeout = setTimeout(() => {
          console.warn('WebSocket 心跳超时,主动重连');
          this.ws.close();
          this.reconnect();
        }, 3000);
      }
    }, 30000);
  }
}
场景三:IAMP 点云数据加载(大文件请求)

“点云文件(.las)可能达 50MB,加载时间受网速影响很大。我用 fetch + AbortController 实现超时中断,并配合 progress 事件展示加载进度。如果用户切走了页面,通过 useEffect 的 cleanup 函数调用 controller.abort()彻底取消正在进行的点云下载,节省用户流量和服务器压力。”


第五层:面试官“变态追问”与终极回应

追问:“Promise.race 超时后,setTimeout 为什么没有被清理?会有什么后果?”

回答:“race 返回后,另一个 Promise 依然会执行完毕,只是结果被忽略了。如果超时 Promise 先返回,setTimeout 的回调已经触发了 reject,但定时器本身还在事件循环里(因为 setTimeout 的回调已经执行完了,不需要清理)。真正需要清理的是 setTimeout 的句柄,所以我在完整版里用了 clearTimeout。”

追问:“如果超时后请求回来了,但页面组件已经卸载了,怎么处理?”

回答:“在 React 中,我会用 useRef + useEffect 的 cleanup 标记组件是否已卸载。请求回来时先判断 isMountedRef.current,如果 false 则直接忽略 setState,避免 React 内存泄漏警告。”

javascript
useEffect(() => {
  let isMounted = true;
  const controller = new AbortController();

  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(data => {
      if (isMounted) setData(data); // 只有组件还在才 setState
    })
    .catch(err => {
      if (err.name !== 'AbortError' && isMounted) {
        setError(err);
      }
    });

  return () => {
    isMounted = false;
    controller.abort(); // 组件卸载时主动取消请求
  };
}, []);

面试现场总结陈词

“面试官,关于前端请求超时,我总结了三个层次:

  1. 基础层:用 Promise.race 实现超时竞速,但要注意 race 不会自动中断底层请求,需要配合 AbortController
  2. 工程层:在 axios 实例上配置 timeout,结合拦截器做统一超时处理错误上报。对 GET 等幂等请求可以加指数退避重试,对 POST 等非幂等请求绝不自动重试,防止数据重复。
  3. 体验层:超时后不要直接抛给用户“网络错误”,而是展示友好的加载状态,如 IRMP 的报告生成,超时后转轮询,让用户看到进度,而不是冰冷的报错框。”

6. Ajax和Fetch的区别?

面试官可能怎么问:

  • "Ajax和Fetch有什么区别?"
  • "Fetch的优缺点?"
  • "Fetch怎么取消请求?"
  • "Axios相比Fetch有什么优势?"

推荐回答框架:

维度Ajax (XMLHttpRequest)Fetch APIAxios
API风格事件驱动,较繁琐基于Promise,简洁基于Promise,功能丰富
进度监控原生支持onprogress需要ReadableStream原生支持onUploadProgress
取消请求原生支持abort()需要AbortController原生支持CancelToken
自动JSON否,需手动JSON.parse需调用.json()自动转换
拦截器请求/响应拦截器
浏览器兼容IE7+IE不支持现代浏览器
javascript
// ========== Ajax (XMLHttpRequest) ==========
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onprogress = (e) => {
  const percent = (e.loaded / e.total) * 100;
  console.log(`下载进度: ${percent}%`);
};
xhr.onload = () => {
  if (xhr.status === 200) {
    console.log(JSON.parse(xhr.responseText));
  }
};
xhr.send();

// 取消请求
xhr.abort();

// ========== Fetch API ==========
// 基础用法
fetch('/api/data')
  .then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
  .then(data => console.log(data))
  .catch(err => console.error(err));

// 取消请求(AbortController)
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
  .then(res => res.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求已取消');
    }
  });

// 5秒后取消
setTimeout(() => controller.abort(), 5000);

// 进度监控(ReadableStream)
fetch('/api/large-file')
  .then(res => {
    const reader = res.body.getReader();
    const contentLength = +res.headers.get('Content-Length');
    let received = 0;

    return new ReadableStream({
      start(controller) {
        function push() {
          reader.read().then(({ done, value }) => {
            if (done) {
              controller.close();
              return;
            }
            received += value.length;
            console.log(`进度: ${(received / contentLength * 100).toFixed(2)}%`);
            controller.enqueue(value);
            push();
          });
        }
        push();
      }
    });
  });

// ========== Axios(项目中推荐) ==========
import axios from 'axios';

const instance = axios.create({
  baseURL: '/api',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' }
});

// 请求拦截器(添加token)
instance.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  error => Promise.reject(error)
);

// 响应拦截器(统一错误处理)
instance.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      //  token过期,跳转登录
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

// 取消请求
const source = axios.CancelToken.source();
instance.get('/data', { cancelToken: source.token });
source.cancel('用户取消操作');

7. 跨域解决方案有哪些?

面试官可能怎么问:

  • "什么是跨域?怎么解决?"
  • "CORS的原理?简单请求和预检请求?"
  • "JSONP的原理和局限性?"
  • "代理怎么解决跨域?"
  • "postMessage是干什么的?"

推荐回答框架:

跨域(CORS)是浏览器的同源策略限制,协议、域名、端口任一不同即跨域。

javascript
// ========== 1. CORS(服务端配置,最常用) ==========
// 服务端响应头
/*
Access-Control-Allow-Origin: https://your-domain.com  // 或 *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true  // 允许携带cookie
Access-Control-Max-Age: 86400  // 预检请求缓存时间
*/

// 简单请求 vs 预检请求(OPTIONS)
// 简单请求:GET/HEAD/POST + 特定Content-Type + 无自定义头
// 预检请求:先发送OPTIONS,确认允许后再发送实际请求

// ========== 2. 代理(开发环境最常用) ==========
// vite.config.js
eexport default {
  server: {
    proxy: {
      '/api': {
        target: 'http://backend-server.com',
        changeOrigin: true,  // 修改请求头中的host
        pathRewrite: { '^/api': '' }
      }
    }
  }
};

// nginx配置
/*
location /api/ {
  proxy_pass http://backend-server.com/;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
}
*/

// ========== 3. JSONP(只支持GET,已过时) ==========
function jsonp(url, callbackName) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    window[callbackName] = (data) => {
      resolve(data);
      document.head.removeChild(script);
      delete window[callbackName];
    };
    script.src = `${url}?callback=${callbackName}`;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// ========== 4. postMessage(跨窗口通信) ==========
// 父窗口
const iframe = document.getElementById('cross-origin-iframe');
iframe.contentWindow.postMessage({ type: 'GET_DATA', payload: {} }, 'https://other-domain.com');

window.addEventListener('message', (e) => {
  if (e.origin !== 'https://other-domain.com') return; // 验证来源
  console.log('收到数据:', e.data);
});

// ========== 5. WebSocket(不受同源策略限制) ==========
const ws = new WebSocket('wss://other-domain.com/socket');
ws.onmessage = (e) => console.log(e.data);

// ========== 结合项目:MMMP监控平台的跨域处理 ==========
/*
场景:内网摄像头RTSP流需要通过WebRTC在公网播放
方案:
1. 使用frp内网穿透,将内网服务映射到公网域名
2. Nginx反向代理,统一入口,解决跨域
3. WebRTC的ICE服务器配置STUN/TURN,处理NAT穿透
*/

8. Webpack打包优化做过哪些?

面试官可能怎么问:

  • "Webpack打包优化做过什么?"
  • "怎么减小打包体积?"
  • "Code Splitting怎么配置?"
  • "Tree Shaking原理?"
  • "Webpack和Vite的区别?"

推荐回答框架:

javascript
// ========== webpack.config.js 优化配置 ==========
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  mode: 'production',

  // 1. 代码分割
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 提取第三方库
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },
        // 提取公共代码
        common: {
          minChunks: 2,
          chunks: 'all',
          enforce: true
        }
      }
    },
    // 2. Tree Shaking
    usedExports: true,
    sideEffects: false
  },

  // 3. 懒加载(动态import)
  // 在路由中使用:const Home = () => import('./views/Home.vue');

  // 4. 压缩
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,  // 移除console
            drop_debugger: true  // 移除debugger
          }
        }
      })
    ]
  },

  // 5. 持久化缓存
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  },

  // 6. 构建速度优化
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'vue$': 'vue/dist/vue.runtime.esm.js'  // 使用runtime版本
    },
    extensions: ['.js', '.vue', '.json'],
    modules: [path.resolve(__dirname, 'node_modules')]  // 限定查找范围
  },

  // 7. 多线程打包
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['thread-loader', 'babel-loader'],
        exclude: /node_modules/
      }
    ]
  },

  plugins: [
    // 8. 分析打包体积
    new BundleAnalyzerPlugin()
  ]
};

// ========== 实际项目优化措施 ==========
/*
在IAMP隧道检测平台中做的优化:
1. Three.js按模块导入(非全量)
   import * as THREE from 'three';  // ❌ 全量导入,体积大
   import { Scene, PerspectiveCamera, WebGLRenderer } from 'three';  // ✅ 按需导入

2. 大文件懒加载
   const PointCloudViewer = () => import('./components/PointCloudViewer.vue');

3. Gzip压缩 + CDN加速
   Nginx配置:gzip on; gzip_types text/css application/javascript;

4. 图片资源优化
   - 使用WebP格式
   - 小图标转base64或SVG
   - 大图使用CDN + 懒加载
*/

// ========== Webpack vs Vite ==========
/*
Webpack:
- 基于打包,开发时也要打包
- 热更新慢(项目越大越明显)
- 生态成熟,配置灵活

Vite:
- 基于ESM,开发时no-bundle
- 热更新快(按需编译)
- 生产环境用Rollup打包
- 配置更简单

在MMMP项目中从Webpack迁移到Vite,构建时间从45s降到3s,
HMR从3s降到200ms,开发效率大幅提升。
*/

9. 首屏加载速度如何优化?

面试官可能怎么问:

  • "首屏加载慢怎么优化?"
  • "FCP、LCP、CLS这些指标了解吗?"
  • "SSR和CSR的区别?"
  • "骨架屏怎么实现?"

推荐回答框架:

javascript
// ========== 1. 资源优化 ==========
// 路由懒加载
const routes = [
  {
    path: '/report',
    component: () => import(/* webpackChunkName: "report" */ './views/Report.vue')
  },
  {
    path: '/3d-viewer',
    component: () => import(/* webpackChunkName: "3d" */ './views/PointCloudViewer.vue')
  }
];

// 图片懒加载
<img v-lazy="imageUrl" :src="placeholder" />

// 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="prefetch" href="/about.js">  // 预取下一页资源

// ========== 2. 代码优化 ==========
// Tree Shaking + 按需加载
// Element Plus 按需导入
import { ElButton, ElTable } from 'element-plus';

// ========== 3. 缓存策略 ==========
// Service Worker缓存(PWA)
// workbox配置
workbox.routing.registerRoute(
  ({ request }) => request.destination === 'image',
  new workbox.strategies.CacheFirst({
    cacheName: 'images',
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60  // 30天
      })
    ]
  })
);

// ========== 4. 网络优化 ==========
// HTTP缓存头
/*
Cache-Control: max-age=31536000, immutable  // 静态资源长期缓存
ETag: "33a64df5"                            // 协商缓存
*/

// CDN + 域名分片
// static1.example.com, static2.example.com

// ========== 5. 渲染优化 ==========
// SSR/SSG(Next.js/Nuxt.js)
// 在华为内审中心项目中使用Next.js的SSG,首屏时间从2.5s降到0.8s

// 骨架屏
/*
<template>
  <div v-if="loading" class="skeleton">
    <div class="skeleton-title"></div>
    <div class="skeleton-content"></div>
  </div>
  <div v-else>
    <!-- 真实内容 -->
  </div>
</template>

.skeleton-title {
  height: 20px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
*/

// ========== 6. 性能指标监控 ==========
/*
FCP (First Contentful Paint):首次内容绘制 < 1.8s
LCP (Largest Contentful Paint):最大内容绘制 < 2.5s
FID (First Input Delay):首次输入延迟 < 100ms
CLS (Cumulative Layout Shift):累积布局偏移 < 0.1
TTFB (Time to First Byte):首字节时间 < 600ms

使用Performance API监控:
*/
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LCP:', entry.startTime);
    // 上报到监控平台
  }
}).observe({ entryTypes: ['largest-contentful-paint'] });

10. 项目中的权限管理怎么实现的?

面试官可能怎么问:

  • "权限管理怎么设计的?"
  • "前端权限和后端权限怎么配合?"
  • "按钮级别的权限怎么控制?"
  • "路由守卫怎么实现?"

推荐回答框架:

javascript
// ========== RBAC(基于角色的访问控制) ==========
// 权限模型:用户 → 角色 → 权限 → 资源

// ========== 1. 路由权限 ==========
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true, roles: ['admin', 'editor'] }
  },
  {
    path: '/report/approve',
    component: () => import('@/views/ReportApprove.vue'),
    meta: { requiresAuth: true, roles: ['admin', 'approver'], permission: 'report:approve' }
  }
];

const router = createRouter({ history: createWebHistory(), routes });

// 路由守卫
router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token');
  const userStore = useUserStore();

  if (to.meta.requiresAuth && !token) {
    next('/login');
    return;
  }

  // 首次加载,获取用户权限
  if (token && !userStore.permissions.length) {
    await userStore.fetchUserInfo();
  }

  // 角色校验
  if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
    next('/403');
    return;
  }

  // 权限点校验
  if (to.meta.permission && !userStore.hasPermission(to.meta.permission)) {
    next('/403');
    return;
  }

  next();
});

// ========== 2. 按钮级别权限(自定义指令) ==========
// directives/permission.js
import { useUserStore } from '@/stores/user';

export const permission = {
  mounted(el, binding) {
    const { value } = binding;  // 'report:delete'
    const userStore = useUserStore();

    if (!userStore.hasPermission(value)) {
      el.parentNode?.removeChild(el);  // 无权限则移除按钮
      // 或者 el.style.display = 'none';
    }
  }
};

// main.js
import { permission } from './directives/permission';
app.directive('permission', permission);

// 使用
<template>
  <el-button v-permission="'report:delete'" type="danger">删除报告</el-button>
  <el-button v-permission="['report:edit', 'report:admin']">编辑</el-button>
</template>

// ========== 3. 菜单权限(动态路由) ==========
// store/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    permissions: [],
    routes: []
  }),

  actions: {
    async fetchUserInfo() {
      const res = await fetchUserInfoAPI();
      this.userInfo = res.data;
      this.permissions = res.data.permissions;  // ['report:view', 'report:edit', ...]
      this.routes = this.generateRoutes(res.data.menus);
    },

    generateRoutes(menus) {
      const accessedRoutes = [];
      menus.forEach(menu => {
        const route = asyncRoutes.find(r => r.path === menu.path);
        if (route) {
          accessedRoutes.push(route);
        }
      });
      return accessedRoutes;
    },

    hasPermission(permission) {
      if (Array.isArray(permission)) {
        return permission.some(p => this.permissions.includes(p));
      }
      return this.permissions.includes(permission);
    }
  }
});

// ========== 4. 数据权限(表格列/搜索条件) ==========
// 根据权限动态显示表格列
const columns = computed(() => {
  const baseColumns = [
    { prop: 'name', label: '名称' },
    { prop: 'status', label: '状态' }
  ];

  if (userStore.hasPermission('report:view_sensitive')) {
    baseColumns.push({ prop: 'cost', label: '成本' });
    baseColumns.push({ prop: 'profit', label: '利润' });
  }

  return baseColumns;
});

// ========== 5. 后端配合(IRMP报告平台实践) ==========
/*
后端返回数据结构:
{
  "role": "approver",
  "permissions": [
    "report:view",
    "report:approve",
    "report:reject"
  ],
  "menus": [
    { "path": "/report/list", "name": "报告列表" },
    { "path": "/report/approve", "name": "报告审批" }
  ]
}

安全原则:
1. 前端权限用于UI控制(隐藏按钮、菜单),提升用户体验
2. 后端必须再次校验权限,防止接口被绕过
3. 敏感操作(删除、审批)必须后端二次确认
*/

============================================================

二、场景题

============================================================

场景1:如果页面出现白屏,如何排查问题?

面试官想考察: 问题排查思路、工具使用、经验积累

javascript
// ========== 系统排查步骤 ==========

// 第1步:确认问题范围
/*
- 所有用户都白屏?还是特定用户?
- 所有页面?还是特定页面?
- 特定浏览器?还是所有浏览器?
- 生产环境?还是开发环境?
*/

// 第2步:浏览器控制台检查
/*
1. 打开F12 → Console
   - 是否有JS报错?(如Cannot read property of undefined)
   - 是否有资源加载失败?(404)
   - 是否有CORS错误?

2. Network面板
   - HTML是否返回200?
   - JS/CSS是否加载成功?
   - 加载时间是否过长?

3. Elements面板
   - body是否为空?
   - 是否有display:none?
   - 是否有visibility:hidden?
*/

// 第3步:常见白屏原因及解决

// 原因1:JS报错导致渲染中断
// 解决:try-catch + 错误边界(React Error Boundary)
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 上报错误到监控平台
    console.error('ErrorBoundary caught:', error, errorInfo);
    reportError({ error, errorInfo, url: window.location.href });
  }

  render() {
    if (this.state.hasError) {
      return <FallbackUI error={this.state.error} />;
    }
    return this.props.children;
  }
}

// Vue3错误处理
app.config.errorHandler = (err, vm, info) => {
  console.error('Vue Error:', err, info);
  reportError({ error: err, info, component: vm?.$options?.name });
};

// 原因2:路由配置错误(如history模式刷新404)
// 解决:Nginx配置try_files
/*
location / {
  try_files $uri $uri/ /index.html;
}
*/

// 原因3:资源加载失败(CDN挂了、路径错误)
// 解决:资源加载监控 + 降级
function loadScript(src, fallbackSrc) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.onload = resolve;
    script.onerror = () => {
      if (fallbackSrc) {
        script.src = fallbackSrc;  // CDN降级到源站
      } else {
        reject(new Error(`Failed to load ${src}`));
      }
    };
    document.head.appendChild(script);
  });
}

// 原因4:内存溢出(如Three.js场景未清理)
// 解决:组件卸载时清理资源
onBeforeUnmount(() => {
  renderer.dispose();
  geometry.dispose();
  material.dispose();
  scene.clear();
});

// 原因5:浏览器兼容(如使用了ES6+语法,IE不支持)
// 解决:Babel转译 + polyfill
// .browserslistrc: > 1%, last 2 versions, not dead

// 第4步:监控与预防
/*
- 接入Sentry/阿里ARMS等错误监控
- 灰度发布,小流量验证
- 自动化测试(E2E)
- 性能预算(Performance Budget)
*/

场景2:用户反馈操作卡顿,从前端角度如何分析?

面试官想考察: 性能分析能力、优化手段、工具使用

javascript
// ========== 分析步骤 ==========

// 第1步:复现问题
/*
- 什么操作卡顿?(点击按钮?滚动?动画?)
- 什么设备?(低端机?)
- 什么网络?(弱网?)
- 什么浏览器?
*/

// 第2步:Performance面板分析
/*
Chrome DevTools → Performance
1. 录制操作过程
2. 查看FPS:绿色条表示流畅,红色条表示卡顿
3. 查看Main线程:长任务(>50ms)会阻塞渲染
4. 查看Timings:FCP、LCP、FID等指标
*/

// 第3步:常见卡顿原因及优化

// 原因1:大量DOM操作(如表格渲染10000行)
// 优化:虚拟滚动
// vue-virtual-scroller / react-window
<RecycleScroller
  class="scroller"
  :items="list"
  :item-size="50"
  key-field="id"
  v-slot="{ item }"
>
  <div class="item">{{ item.name }}</div>
</RecycleScroller>

// 原因2:频繁重排重绘
// 优化:批量操作 + 使用transform
// ❌ 频繁修改样式,触发重排
for (let i = 0; i < 1000; i++) {
  element.style.left = i + 'px';  // 每次都重排
}

// ✅ 使用requestAnimationFrame + transform
function animate() {
  let start = null;
  function step(timestamp) {
    if (!start) start = timestamp;
    const progress = timestamp - start;
    element.style.transform = `translateX(${Math.min(progress / 10, 1000)}px)`;
    if (progress < 10000) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

// 原因3:内存泄漏(闭包、事件未解绑、定时器未清理)
// 优化:组件卸载时清理
onBeforeUnmount(() => {
  // 清除定时器
  clearInterval(timer);
  clearTimeout(timeout);

  // 解绑事件
  window.removeEventListener('resize', handleResize);

  // 取消订阅
  subscription.unsubscribe();

  // 清理WebSocket
  ws.close();

  // 清理Three.js资源
  renderer.dispose();
});

// 原因4:大数据量计算阻塞主线程
// 优化:Web Worker
const worker = new Worker('./worker.js');
worker.postMessage({ points: pointCloudData });  // 发送数据到Worker
worker.onmessage = (e) => {
  const processed = e.data;  // 接收处理结果
  render(processed);
};

// worker.js
self.onmessage = (e) => {
  const { points } = e.data;
  // 在Worker线程中处理大数据
  const result = heavyComputation(points);
  self.postMessage(result);
};

// 原因5:图片/视频资源过大
// 优化:懒加载 + 压缩 + WebP
const imgObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      imgObserver.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imgObserver.observe(img);
});

// 原因6:React/Vue组件不必要的重渲染
// 优化:useMemo / useCallback / memo / computed
// React
const MemoizedComponent = React.memo(MyComponent, (prev, next) => {
  return prev.id === next.id;  // 自定义比较
});

// Vue
const filteredList = computed(() => {
  return list.value.filter(item => item.status === 'active');
});

// 第4步:性能监控
// 使用Performance API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {  // 长任务
      console.warn('Long task detected:', entry.duration);
      reportLongTask(entry);
    }
  }
});
observer.observe({ entryTypes: ['longtask'] });
最近更新