前端工程化与性能优化 面试核心

系统掌握工程化体系、构建原理、性能优化与监控,从容应对中高级前端面试

1 前端工程化概述

小作坊 vs 现代工厂

一个前端"小作坊":手动复制文件到服务器,代码没有统一格式,全靠人肉测试,部署靠 FTP 上传。

一个前端"现代工厂":代码有 ESLint 自动检查,提交走 Git Hook,PR 自动跑测试,合并后自动构建部署,线上有实时监控报警。

工程化的本质:用工具和流程替代人工判断,让质量成为默认选项而非偶然结果。

工程化四大支柱

🧩
模块化
代码按功能拆分独立模块
ESM / CommonJS / AMD
📦
组件化
UI 拆分为可复用组件
Vue / React 组件体系
📏
规范化
统一的编码/提交/分支规范
ESLint / Prettier / Husky
🤖
自动化
构建/测试/部署自动执行
CI/CD / Git Hooks

工程化流水线

📐 工程化完整流水线
📝 代码编写
🔍 ESLint/Prettier
🧪 单元测试
⚙️ 构建打包
🚀 自动部署
📊 监控报警

每一步都应该是自动化的、可重复的、可回溯的

2 构建工具原理

Webpack 构建流程

📐 Webpack 构建流水线
Entry 入口
📄 index.js → 从入口文件开始,递归分析依赖图
Loader 编译
.vue → vue-loader  |  .scss → sass-loader  |  .ts → ts-loader  |  非 JS → JS
Plugin 插件
HtmlWebpackPlugin(生成HTML) | MiniCssExtract(CSS抽取) | DefinePlugin(环境变量)
Output 产出
📦 dist/ → bundle.[hash].js | vendor.[hash].js | main.[hash].css
// webpack.config.js 核心配置
const path = require('path');

module.exports = {
  entry: './src/index.js',         // 入口
  output: {
    path: path.resolve('dist'),     // 输出目录
    filename: '[name].[contenthash].js',
    clean: true,                   // 每次构建清理 dist
  },
  module: {
    rules: [
      { test: /\.vue$/, use: 'vue-loader' },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
    new MiniCssExtractPlugin(),
  ],
};

Vite:下一代构建工具

📐 Webpack vs Vite 对比
Webpack

🔨 开发模式:打包所有模块 → 启动 dev server

📦 构建:自己实现的打包器

⏱️ 冷启动:项目越大越慢(分钟级)

🔄 HMR:需要重新打包变更模块链

Vite

开发模式:原生 ESM,按需编译

📦 构建:Rollup(生产级打包)

🚀 冷启动:毫秒级,不受项目规模影响

🔄 HMR:精确到单个模块,极速

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  server: { port: 3000, open: true },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['vue', 'vue-router', 'pinia'],
        },
      },
    },
  },
});

Tree Shaking

📐 Tree Shaking 前后对比
❌ Tree Shaking 前
✅ import { add } — 使用
❌ import { subtract } — 未使用
❌ import { multiply } — 未使用
✅ import { divide } — 使用
❌ import { modulo } — 未使用
📦 Bundle: 5.2 KB
✅ Tree Shaking 后
✅ export function add() { ... }
✅ export function divide() { ... }
~~ 已摇掉 ~~~
📦 Bundle: 1.8 KB (-65%)
关键条件:Tree Shaking 依赖 ESM 的静态结构import/export),CommonJS 的 require() 是动态的,无法静态分析。

Code Splitting 代码分割

// 1. 动态 import — 路由级懒加载
const routes = [
  { path: '/dashboard', component: () => import('./views/Dashboard.vue') },
  { path: '/settings',  component: () => import('./views/Settings.vue') },
];

// 2. Webpack splitChunks 配置
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendor: {
        test: /node_modules/,
        name: 'vendor',
        priority: 10,
      },
      common: {
        minChunks: 2,
        name: 'common',
        priority: 5,
      },
    },
  },
}

案例:构建产物从 5MB 优化到 800KB

Step 1 — 分析
rollup-plugin-visualizer 发现 lodash 全量引入占 1.2MB
Step 2 — Tree Shaking
改用 lodash-es + ESM,摇掉未用函数 → 减少 1MB
Step 3 — Code Splitting
路由级懒加载 + splitChunks → 首屏只加载核心 200KB
Step 4 — 压缩优化
Terser 压缩 + gzip → 总体积从 5MB 降到 800KB

3 代码规范

ESLint + Prettier 配置

// .eslintrc.js
module.exports = {
  root: true,
  env: { browser: true, node: true, es2022: true },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',   // 必须放最后,关闭与 Prettier 冲突的规则
  ],
  rules: {
    'no-console': ['warn', { allow: ['warn', 'error'] }],
    'no-unused-vars': 'error',
    'vue/multi-word-component-names': 'off',
  },
};

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100
}

Husky + lint-staged (Pre-commit Hook)

📐 Git Hook 工作流
git add
git commit
⚡ Husky 触发 pre-commit
🔍 lint-staged 只检查暂存文件
✅ 通过则提交成功
// package.json
{
  "lint-staged": {
    "*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
    "*.{css,scss}": ["prettier --write"]
  }
}

# 安装 & 配置 Husky
npx husky init
# 创建 pre-commit hook
echo "npx lint-staged" > .husky/pre-commit

Commitlint (规范提交信息)

# commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'feat',     // 新功能
      'fix',      // 修复
      'docs',     // 文档
      'style',    // 格式(不影响逻辑)
      'refactor', // 重构
      'perf',     // 性能优化
      'test',     // 测试
      'chore',    // 构建/工具
    ]],
  },
};

# .husky/commit-msg
npx --no -- commitlint --edit $1

案例:没有规范导致代码风格混乱

某项目三人协作,无 ESLint:A 用双引号 + 分号,B 用单引号无分号,C 用 Tab 缩进。Git diff 充斥格式变更,CR 无法聚焦逻辑,线上因 undefined.xx 报错(ESLint 能检测到)。

引入 ESLint + Husky 后:提交即检查,风格统一,低级错误清零。

4 CI/CD 流水线

流水线图解

📐 CI/CD 完整流水线
1
Push / PR — 开发者推送代码到仓库,触发 CI Pipeline
2
Lint & Type Check — ESLint、TypeScript 类型检查
3
Unit & E2E Test — 运行测试套件,覆盖率检查
4
Build — npm run build,产出静态资源
5
Deploy — 自动部署到对应环境 (Staging / Production)
6
Notify & Monitor — 钉钉/飞书通知 + Sentry 错误追踪

GitHub Actions 示例

# .github/workflows/ci.yml
name: CI Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run lint
      - run: npm run test:unit
      - run: npm run build
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

Jenkinsfile 示例

pipeline {
  agent { docker { image 'node:20-alpine' } }
  stages {
    stage('Install')  { steps { sh 'npm ci' } }
    stage('Lint')    { steps { sh 'npm run lint' } }
    stage('Test')    { steps { sh 'npm run test:unit' } }
    stage('Build')   { steps { sh 'npm run build' } }
    stage('Deploy')  {
      steps {
        sh 'rsync -az --delete dist/ server:/var/www/html/'
      }
    }
  }
  post {
    failure { // 通知开发团队 }
    success { // 部署成功通知 }
  }
}

环境管理策略

环境用途数据部署方式
Development日常开发调试Mock / 本地手动 / Vite dev
Staging预发布验证脱敏生产数据合并到 release 分支自动部署
Production线上用户真实数据Tag / 合并到 main 自动部署

案例:手动部署导致线上事故

某公司手动 FTP 部署:开发者上传时覆盖了别人的版本,导致线上回退到旧代码,故障持续 2 小时。

引入 CI/CD 后:每次部署可追溯、可回滚,staging 环境先验证再上线。部署时间从 30 分钟缩短到 3 分钟,事故率降为零。

5 性能指标 (Core Web Vitals)

Google 定义的核心 Web 指标,直接影响 SEO 排名和用户体验。

核心指标仪表盘

📐 Core Web Vitals 指标范围
LCP (Largest Contentful Paint) 最大内容绘制
Good < 2.5s
2.5s - 4s
> 4s
INP (Interaction to Next Paint) 交互响应延迟
Good < 200ms
200ms - 500ms
> 500ms
CLS (Cumulative Layout Shift) 累积布局偏移
Good < 0.1
0.1 - 0.25
> 0.25
FCP (First Contentful Paint) 首次内容绘制
Good < 1.8s
1.8s - 3s
> 3s
TTFB (Time to First Byte) 首字节时间
Good < 800ms
800ms - 1.8s
> 1.8s
TTI (Time to Interactive) 可交互时间
Good < 3.8s
3.8s - 7.3s
> 7.3s

测量工具

🔦
Lighthouse
Chrome DevTools 集成
综合评分报告
📊
Web Vitals JS
web-vitals npm 包
真实用户指标采集
🛠️
Chrome DevTools
Performance / Network
Performance 面板分析
📈
Search Console
Google 站长工具
Core Web Vitals 报告
// 使用 web-vitals 库采集真实指标
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP((metric) => {
  // 发送到监控后端
  sendToAnalytics({ name: 'LCP', value: metric.value, rating: metric.rating });
});
onINP((metric) => { /* ... */ });
onCLS((metric) => { /* ... */ });

6 性能优化实战

加载优化

Resource Hints 资源提示

<head>
  <!-- DNS 预解析 -->
  <link rel="dns-prefetch" href="//cdn.example.com">
  <!-- 预连接 (DNS + TCP + TLS) -->
  <link rel="preconnect" href="https://api.example.com" crossorigin>
  <!-- 预加载关键资源 -->
  <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
  <!-- 空闲时预获取 -->
  <link rel="prefetch" href="/js/about-page.js">
</head>

图片优化

<!-- 响应式图片 + WebP + 懒加载 -->
<picture>
  <source srcset="hero.webp" type="image/webp">
  <source srcset="hero.jpg" type="image/jpeg">
  <img
    src="hero.jpg"
    alt="Hero"
    loading="lazy"
    width="800" height="400"
    srcset="hero-400w.jpg 400w, hero-800w.jpg 800w"
    sizes="(max-width: 600px) 400px, 800px"
  />
</picture>
关键:设置 widthheight 属性可以帮助浏览器预留空间,避免 CLS(布局偏移)。

字体优化

<style>
  @font-face {
    font-family: 'MainFont';
    src: url('/fonts/main-subset.woff2') format('woff2');
    font-display: swap;    /* 先用系统字体,加载完再替换 */
    unicode-range: U+4E00-9FFF; /* 只加载中文字符子集 */
  }
</style>

Bundle 分析

// vite.config.ts — 使用 rollup-plugin-visualizer 分析
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({ open: true, gzipSize: true }),
  ],
});

运行时优化

防抖与节流

📐 Debounce vs Throttle 时序对比
🔴 Debounce (防抖) — 停止触发后才执行一次
触发:
← 连续触发5次
执行:
← 停止后执行1次
🟢 Throttle (节流) — 固定间隔执行,稀释频率
触发:
← 连续触发7次
执行:
← 固定间隔执行3次
Debounce 场景
搜索框输入、窗口 resize、表单验证
Throttle 场景
滚动事件、拖拽、射击游戏、scroll 监听
// 防抖实现
function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// 节流实现
function throttle(fn, interval = 300) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn(...args);
    }
  };
}

虚拟滚动 (Virtual Scrolling)

// 原理:只渲染可视区域的 DOM,10万条数据也只渲染 20-30 个 DOM 节点
const visibleItems = computed(() => {
  const startIdx = Math.floor(scrollTop.value / itemHeight);
  const endIdx = startIdx + visibleCount;
  return allItems.slice(startIdx, endIdx);
});

requestAnimationFrame 动画

// 不要用 setInterval 做动画,用 rAF 与浏览器刷新率同步
function animate() {
  // 更新动画状态
  element.style.transform = `translateX(${x}px)`;
  if (x < target) {
    requestAnimationFrame(animate); // 递归调用,约60fps
  }
}
requestAnimationFrame(animate);

Web Worker 重计算

// 主线程
const worker = new Worker('/workers/heavy-compute.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
  // 拿到结果,不阻塞 UI
  updateUI(e.data.result);
};

// heavy-compute.js
self.onmessage = (e) => {
  const result = heavyProcessing(e.data);
  self.postMessage({ result });
};

渲染优化

关键渲染路径优化

关键渲染路径流程
HTML 解析
DOM Tree
CSSOM
Render Tree
Layout 布局
Paint 绘制
Composite 合成
优化策略:CSS 放 <head>(尽早构建 CSSOM)、JS 放 <body> 底部或加 defer(避免阻塞解析)。

CSS Containment & will-change

/* CSS Containment — 限制浏览器重排/重绘范围 */
.card-list {
  contain: layout style paint;  /* 隔离渲染边界 */
}

.animated-element {
  will-change: transform, opacity;  /* 提示浏览器提前优化 */
}

/* 注意:不要滥用 will-change,它本身也有内存开销 */

案例:首屏加载从 6s 优化到 1.5s — 逐步优化过程

Step 1 — 分析瓶颈 (6s)
Lighthouse 报告:JS bundle 2.5MB、未压缩图片 3MB、14 个渲染阻塞资源
Step 2 — 资源压缩 (→ 4s)
图片 WebP + 压缩、开启 gzip、JS/CSS 压缩 → 资源总量减少 60%
Step 3 — 代码分割 (→ 2.5s)
路由懒加载 + splitChunks → 首屏 JS 从 2.5MB 降到 300KB
Step 4 — 预加载 + CDN (→ 1.8s)
关键 CSS 内联、字体 preload、静态资源 CDN + 缓存策略
Step 5 — SSR 首屏渲染 (→ 1.5s)
引入 SSR/SSG,HTML 直出,FCP 从 2.5s 降到 0.8s
📐 性能优化策略树
⚡ 前端性能优化
📦 加载优化
资源压缩 (gzip/br)
代码分割 + 懒加载
Tree Shaking
图片优化 (WebP/AVIF)
CDN + HTTP/2
⚡ 运行时优化
虚拟滚动
防抖 / 节流
requestAnimationFrame
Web Worker
内存管理 (避免泄漏)
🎨 渲染优化
关键渲染路径优化
CSS containment
will-change 提示
合成层优化 (transform/opacity)
SSR / SSG
🌐 网络优化
Resource Hints
Service Worker 缓存
HTTP/2 多路复用
预渲染 / 骨架屏

7 监控与埋点

错误监控

// 1. Sentry — 最流行的错误监控平台
import * as Sentry from '@sentry/vue';

Sentry.init({
  dsn: 'https://xxx@sentry.io/project',
  integrations: [Sentry.browserTracingIntegration()],
  tracesSampleRate: 0.1,  // 采样 10%
  release: 'my-app@1.2.3',
});

// 2. 全局错误捕获
// JS 运行时错误
window.onerror = (msg, source, line, col, error) => {
  reportError({ type: 'js_error', msg, source, line, col, stack: error?.stack });
  return false;
};

// Promise 未捕获异常
window.addEventListener('unhandledrejection', (e) => {
  reportError({ type: 'promise_error', reason: e.reason });
});

// 3. Vue 全局错误处理
app.config.errorHandler = (err, vm, info) => {
  Sentry.captureException(err);
};

性能监控

// Navigation Timing API — 页面加载各阶段耗时
const [nav] = performance.getEntriesByType('navigation');
const metrics = {
  dns: nav.domainLookupEnd - nav.domainLookupStart,
  tcp: nav.connectEnd - nav.connectStart,
  ttfb: nav.responseStart - nav.requestStart,
  download: nav.responseEnd - nav.responseStart,
  domParse: nav.domInteractive - nav.responseEnd,
  load: nav.loadEventEnd - nav.startTime,
};

// Resource Timing API — 单个资源加载耗时
const resources = performance.getEntriesByType('resource');
resources.forEach(r => {
  if (r.duration > 3000) {
    reportSlowResource({ name: r.name, duration: r.duration });
  }
});

用户行为埋点

// 基础埋点策略
const tracker = {
  // 页面浏览
  pageView(page) {
    send({ event: 'pv', page, timestamp: Date.now() });
  },
  // 用户点击
  click(target, metadata) {
    send({ event: 'click', target, ...metadata });
  },
  // 自定义事件
  track(eventName, data) {
    send({ event: eventName, ...data });
  },
};

// 声明式埋点:Vue 自定义指令
app.directive('track', {
  mounted(el, binding) {
    el.addEventListener('click', () => {
      tracker.track(binding.value);
    });
  },
});
// <button v-track="'submit_order'">提交订单</button>

案例:上线后白屏了但用户没反馈

某电商大促上线后,部分 iOS 14 用户白屏,但客服没接到投诉(用户直接流失了)。没有监控意味着你永远不知道你不知道什么

接入 Sentry + 自定义性能监控后:

没有监控的系统 = 盲人开坦克。

8 面试题精选 (25道)

点击问题展开答案 ↓

1什么是前端工程化?它解决的核心问题是什么?+
前端工程化是通过工具链和标准化流程来管理前端项目全生命周期的方法论。

核心解决问题:
协作效率:统一的代码规范和流程,减少沟通成本
质量保障:自动化测试、lint 检查,减少低级错误
开发体验:热更新、自动构建,让开发者聚焦业务
可维护性:模块化/组件化架构,方便长期演进
部署安全:CI/CD 自动化,可回滚、可追溯
2Webpack 的构建流程是怎样的?+
核心流程
1. 初始化参数:从配置文件和 shell 语句合并参数
2. 开始编译:用参数初始化 Compiler 对象,加载所有 Plugin
3. 确定入口:根据 entry 找到所有入口文件
4. 编译模块:从入口出发,调用 Loader 翻译所有模块,递归查找依赖
5. 完成模块编译:得到每个模块翻译后的内容和依赖关系
6. 输出资源:根据依赖关系组装 Chunk,再转换成单独文件
7. 输出完成:在确定好输出内容后,根据配置的 output 路径和文件名写入文件系统

Loader vs Plugin:Loader 是文件转换器(sass → css),Plugin 是插件(在整个生命周期中钩入事件)。
3Vite 为什么比 Webpack 快?+
开发模式
• Webpack 先打包所有模块再启动 dev server,项目越大越慢
• Vite 利用浏览器原生 ESM,按需编译,启动时只需处理入口文件
• Vite 使用 esbuild(Go 编写)做依赖预构建,比 JS 工具快 10-100 倍

HMR
• Webpack:修改文件后需要重新构建涉及的模块链
• Vite:只精确重新编译修改的单个模块,浏览器只请求该模块

生产构建:Vite 使用 Rollup,天然支持 Tree Shaking,产出更小。
4什么是 Tree Shaking?它的原理和限制是什么?+
原理:基于 ESM 的静态结构,在编译阶段就能确定哪些 export 被使用,删除未使用的代码("摇掉"枯叶)。

前提条件
• 必须使用 ESM(import/export),CommonJS 不行
• 模块不能有副作用(或在 package.json 标记 sideEffects: false

限制
• 动态导入、反射(如 Object.keys(imported))会导致失效
• 有副作用的模块(如 polyfill、CSS 导入)需要标记
• 第三方库如果没有 ESM 版本则无法 Tree Shake
5Code Splitting 有哪些方式?+
三种方式
1. 入口分割:配置多个 entry,手动拆分(不推荐,有公共依赖重复)
2. 动态 import():路由级懒加载,最常用
3. splitChunks:Webpack 自动分析,提取公共依赖

最佳实践
• 路由级别懒加载:() => import('./views/About.vue')
• 第三方库单独打包:vendor chunk
• 公共模块提取:被 2+ chunk 引用的模块提取为 common chunk
• 预加载策略:import(/* webpackPrefetch: true */ './module')
6Loader 和 Plugin 的区别?+
Loader — 文件转换器
• 将非 JS 文件转换为 Webpack 能处理的模块
• 执行顺序:从右到左、从下到上
• 例:babel-loader(ES6+→ES5)、css-loader(CSS→JS 模块)、file-loader(文件→URL)

Plugin — 生命周期钩子
• 监听 Webpack 广播的事件,在合适的时机通过 Webpack 的 API 改变输出结果
• 可以贯穿整个构建周期
• 例:HtmlWebpackPlugin(生成 HTML)、MiniCssExtractPlugin(CSS 抽取为文件)

一句话:Loader 做翻译,Plugin 做增强。
7ESLint 和 Prettier 的分工和冲突解决?+
分工
ESLint:代码质量检查(未使用变量、可能导致 bug 的写法)
Prettier:代码格式化(缩进、换行、引号、分号)

冲突解决
• 安装 eslint-config-prettier,它关闭所有和 Prettier 冲突的 ESLint 规则
• 在 extends 中放在最后extends: [..., 'prettier']
• 这样 ESLint 只管质量,Prettier 只管格式,互不干扰
8Husky + lint-staged 的工作原理?+
工作流
1. Husky:在 .husky/ 目录注册 Git Hooks
2. 开发者执行 git commit 时,触发 pre-commit hook
3. lint-staged:只对暂存区(staged)的文件运行 lint/format
4. 通过则提交成功,失败则阻止提交

为什么用 lint-staged 而不是全量 lint
• 大项目全量 lint 可能需要几十秒
• 只检查改动的文件,速度快、反馈及时
• 全量检查放在 CI 中保证全局质量
9CI/CD 是什么?为什么前端需要它?+
CI (Continuous Integration):持续集成 — 代码合并到主干时自动运行 lint/test/build
CD (Continuous Deployment/Delivery):持续部署 — 构建通过后自动部署到服务器

前端为什么需要
质量门禁:PR 合并前必须通过所有检查
部署自动化:避免手动 FTP 上传(易出错、不可追溯)
环境一致:开发/staging/production 使用相同的构建流程
快速回滚:出问题时秒级回退到上一版本
可追溯:每次部署对应的 commit、构建日志都有记录
10Core Web Vitals 有哪些指标?各有什么含义?+
三大核心指标
LCP(Largest Contentful Paint):最大内容绘制 — 页面主要内容加载速度。目标 < 2.5s
INP(Interaction to Next Paint):交互到下次绘制延迟 — 响应用户交互的速度。目标 < 200ms
CLS(Cumulative Layout Shift):累积布局偏移 — 页面视觉稳定性。目标 < 0.1

辅助指标
FCP:首次内容绘制,目标 < 1.8s
TTFB:首字节时间,目标 < 800ms
TTI:可交互时间,目标 < 3.8s

影响:Google 将这些指标纳入 SEO 排名因素。
11如何优化 LCP?+
LCP 元素通常是:大图片、背景图、大段文字块

优化策略
1. 图片优化:使用 WebP/AVIF、设置尺寸避免重排、用 loading="eager" 对首屏图片
2. 关键资源预加载<link rel="preload"> LCP 图片/字体
3. CSS 优化:内联关键 CSS,避免渲染阻塞
4. JS 延迟:非关键 JS 用 deferasync
5. CDN:减少 TTFB
6. SSR/SSG:服务端渲染直接输出 HTML
12如何优化 CLS(布局偏移)?+
常见原因和解决方案
1. 图片无尺寸:设置 width/heightaspect-ratio
2. 动态注入内容:预留空间(skeleton/占位符),避免已渲染内容被推下去
3. Web 字体闪烁font-display: swap 或使用 size-adjust
4. 异步加载的广告/嵌入:给容器设定固定高度
5. 动画:使用 transform(合成属性)代替 top/left(触发布局)
13preload、prefetch、preconnect 的区别?+
preload:当前页面一定需要的资源,高优先级立即加载
  例:<link rel="preload" href="font.woff2" as="font" crossorigin>

prefetch:未来页面可能需要的资源,浏览器空闲时低优先级加载
  例:路由懒加载组件的预获取

preconnect:提前建立连接(DNS + TCP + TLS),不下载资源
  例:<link rel="preconnect" href="https://api.example.com">

dns-prefetch:只预解析 DNS,最轻量
14防抖和节流的区别?各自适用场景?+
防抖 (debounce):连续触发后只执行最后一次。就像电梯等人,有人进来就重新等。
• 场景:搜索框联想、窗口 resize、表单实时校验

节流 (throttle):固定时间间隔执行一次。就像水龙头限制流量。
• 场景:scroll 事件、拖拽、射击游戏点击

手写核心实现:防抖用 setTimeout + clearTimeout;节流用时间戳比较或定时器。
15虚拟滚动的原理?什么场景需要?+
原理
• 计算当前滚动位置对应的可视范围(startIndex ~ endIndex)
• 只渲染这个范围内的 DOM 节点(通常 20-30 个)
• 用一个总高度的容器模拟完整滚动条
• 滚动时动态更新渲染列表

适用场景:数据量 > 1000 条的长列表(聊天记录、日志表格、通讯录)

关键计算
startIndex = Math.floor(scrollTop / itemHeight)
visibleCount = Math.ceil(containerHeight / itemHeight)
• 使用 transform: translateY(offset) 定位
16关键渲染路径是什么?如何优化?+
路径:HTML → DOM Tree + CSS → CSSOM → Render Tree → Layout → Paint → Composite

优化策略
1. CSS 放 head:尽早构建 CSSOM,避免渲染阻塞
2. JS 放 body 底部或 defer:避免阻塞 DOM 解析
3. 内联关键 CSS:首屏渲染所需 CSS 直接嵌入 HTML
4. async/defer:非关键脚本异步加载
5. 减少关键资源数量:合并 CSS、移除无用 CSS
6. 减少关键路径长度:减少关键资源之间的依赖链
17前端错误监控怎么做?+
错误分类和捕获
1. JS 运行时错误window.onerror / window.addEventListener('error')
2. Promise 异常window.addEventListener('unhandledrejection')
3. 资源加载失败window.addEventListener('error', ..., true)(捕获阶段)
4. 框架错误:Vue errorHandler / React ErrorBoundary
5. 接口错误:拦截 fetch/XHR

Sentry 最佳实践
• 配置 source map 上传,定位压缩后代码的原始位置
• 设置 release 版本号,区分不同版本的错误
• 配置采样率避免数据量过大
• 添加用户信息、面包屑(操作路径)方便复现
18如何做前端性能监控?+
两个层面

1. 实验室数据(开发阶段):
• Lighthouse CI:集成到 CI 中,每次构建自动跑分
• Chrome DevTools Performance 面板:火焰图分析

2. 真实用户数据(RUM,线上):
web-vitals 库采集 LCP/INP/CLS
PerformanceObserver API 采集自定义指标
Navigation Timing API:DNS/TCP/TTFB/Load 各阶段耗时
Resource Timing API:单个资源的加载详情

数据上报:用 navigator.sendBeacon 在页面卸载时保证数据不丢失。
19什么是 SSR?和 CSR、SSG 的区别?+
CSR (Client-Side Rendering):浏览器下载空 HTML + JS,JS 在客户端渲染页面。
• 优点:交互流畅(SPA)
• 缺点:首屏慢、SEO 差

SSR (Server-Side Rendering):服务器执行 JS 渲染 HTML 返回给浏览器。
• 优点:首屏快、SEO 好
• 缺点:服务器压力大、TTFB 增加

SSG (Static Site Generation):构建时预渲染为静态 HTML。
• 优点:最快(CDN 直出)、SEO 好
• 缺点:只适合内容相对固定的页面

选型:内容站 → SSG、动态应用 → SSR+CSR 混合、后台管理 → CSR。
20图片优化有哪些手段?+
格式:JPEG → WebP(小 25-35%)→ AVIF(小 50%)
响应式srcset + sizes,根据屏幕宽度加载不同尺寸
懒加载loading="lazy",首屏外图片延迟加载
尺寸预留:设置 width/height,避免 CLS
CSS 替代:简单图形用 CSS/SVG 代替图片
CDN 图片处理:阿里云 OSS / Cloudinary 按参数裁剪压缩
渐进式 JPEG:先低清后高清,感知加载更快
21Webpack 的热更新 (HMR) 原理?+
流程
1. Webpack dev server 和浏览器之间建立 WebSocket 连接
2. 文件修改 → Webpack 重新编译该模块 → 生成 hot-update.json(manifest)和 hot-update.js(补丁)
3. 通过 WebSocket 通知浏览器:hash 变更
4. 浏览器请求 hot-update.json 获取变更列表
5. 请求 hot-update.js 获取模块代码
6. HMR Runtime 替换旧模块,执行 module.hot.accept 回调

关键:只替换变更模块,不刷新页面,保持应用状态。
22如何排查和解决内存泄漏?+
常见原因
• 未清除的 setInterval/setTimeout
• 未移除的事件监听器
• 闭包引用了不需要的大对象
• 脱离 DOM 的引用(删除了 DOM 但 JS 还持有引用)
• 全局变量不断累积

排查工具
• Chrome DevTools → Memory 面板 → Heap Snapshot
• 对比两次快照,找到只增不减的对象
Performance 面板录制,观察内存是否持续上升

预防:组件销毁时清理副作用(Vue 的 onUnmounted / React 的 useEffect cleanup)。
23什么是 source map?生产环境怎么处理?+
是什么:构建后的代码(压缩/混淆)到源代码的映射文件,方便调试定位。

生产环境策略
不发布到 CDN:source map 只上传到错误监控平台(Sentry),不暴露给用户
• 配置 hidden-source-map:生成 map 但 CSS/JS 中不引用
• Sentry 的 @sentry/webpack-plugin 构建时自动上传

安全考虑:source map 暴露源代码,绝不应对外公开。使用 devtool: 'hidden-source-map'
24如何设计一个前端监控体系?+
三大监控

1. 错误监控
• JS 错误、Promise 异常、资源加载失败、接口错误
• 工具:Sentry、自建上报

2. 性能监控
• Core Web Vitals (LCP/INP/CLS)
• 页面加载各阶段耗时 (Navigation Timing)
• 接口响应时间

3. 业务监控
• PV/UV、关键路径转化率
• 用户行为路径(面包屑)

技术要点
• 采样率控制(避免数据量爆炸)
navigator.sendBeacon 保证卸载时上报
• 数据聚合 + 报警规则(错误率超过阈值触发告警)
25如何从零开始优化一个加载 6 秒的页面?说说思路。+
系统化优化思路

Step 1 — 测量 & 分析:Lighthouse 跑分 → Network 面板看资源 → 确定瓶颈
Step 2 — 快速见效:开启 gzip/brotli、图片压缩、CDN → 通常能减少 50%
Step 3 — JS 优化:Tree Shaking、Code Splitting、懒加载 → 首屏 JS 降到最小
Step 4 — CSS 优化:内联关键 CSS、移除无用 CSS
Step 5 — 资源提示:preload 关键资源、preconnect API 域名
Step 6 — 缓存策略:静态资源强缓存 + 文件名 hash、Service Worker
Step 7 — 渲染优化:骨架屏/SSR 减少 FCP、避免布局偏移
Step 8 — 持续监控:接入 web-vitals 持续采集,回归防止倒退

核心原则:先量化再优化,先低成本再高成本,持续测量。


前端工程化与性能优化 — 面试备战手册 | 全栈知识库