每日面试题(2026-05-15)- 前端

一、React (T0)

1.1 hooks原理

题目

请实现一个简化版的 useState Hook,并说明 React Hooks 的实现原理核心机制(链表、Fiber节点关联等)。

参考答案

javascript 复制代码
// 简化版 useState 实现
let isMount = true; // 是否首次渲染
let workInProgressHook = null; // 当前工作的 hook 指针
let firstWorkInProgressHook = null; // 链表头指针

function useState(initialState) {
  let hook;

  if (isMount) {
    // 首次挂载:创建 hook 节点并加入链表
    hook = {
      state: initialState,
      next: null,
      queue: [] // 存储 setState 的更新队列
    };

    if (!firstWorkInProgressHook) {
      firstWorkInProgressHook = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    // 更新阶段:按顺序取出已有的 hook
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  // 基础状态值
  let baseState = hook.state;

  // 处理更新队列
  hook.queue.forEach(action => {
    baseState = typeof action === 'function' ? action(baseState) : action;
  });
  hook.queue = [];

  // 设置新状态的方法
  const setState = (action) => {
    hook.queue.push(action);
    // 触发重新渲染的逻辑(实际React中会调度更新)
  };

  return [baseState, setState];
}

详细解析

React Hooks 核心原理:

  1. 链表结构: 每个组件内部维护一个 hooks 链表,React 通过链表按顺序记录每个 hook 的状态,确保每次渲染顺序一致。

  2. Fiber 节点关联 : 组件对应的 Fiber 节点通过 memoizedState 属性指向 hooks 链表的头节点,实现状态与组件的关联。

  3. 规则限制: hooks 只能在组件顶层调用,不能在条件语句/循环/嵌套函数中调用 ------ 因为链表是按顺序遍历的,位置必须固定。

  4. 更新机制 : setState 会将更新函数加入队列,React 批量处理时执行这些函数得到新状态,然后触发重新渲染。


1.2 React18并发

题目

请解释 React 18 的 并发渲染(Concurrent Rendering) 机制,以及它带来的核心变化和实际应用场景。

参考答案

并发渲染的核心概念:

  1. 可中断渲染: React 18 之前,渲染是不可中断的整块执行;并发模式下,渲染可以被高优先级更新打断。

  2. 时间切片 (Time Slicing) : 通过 scheduler 包将渲染工作分成多个小任务,在浏览器空闲时间执行,避免阻塞主线程。

  3. Suspense + Streaming SSR : 服务端流式渲染,支持 loading 状态逐步展示内容。

关键 API:

jsx 复制代码
import { createRoot } from 'react-dom/client';
import { Suspense } from 'react';

// 1. 并发根节点
const root = createRoot(document.getElementById('root'));
root.render(<App />);

// 2. Suspense 流式加载
<Suspense fallback={<Spinner />}>
  <ProductDetails productId={id} />  {/* 可中断加载 */}
</Suspense>

// 3. useTransition - 标记非紧急更新
import { useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  
  const [activeTab, setActiveTab] = useState('posts');
  
  const handleClick = (tab) => {
    startTransition(() => {
      setActiveTab(tab);  // 标记为可中断的低优先级更新
    });
  };
}

// 4. useDeferredValue - 延迟更新非关键 UI
import { useDeferredValue } from 'react';

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  // 立即响应用户输入,延迟更新搜索结果
  return (
    <div>
      <RealTimeSuggestions query={query} />
      <HeavySearchResults query={deferredQuery} />
    </div>
  );
}

详细解析

并发渲染解决的问题:

场景 React 17 React 18
大量列表渲染时输入框响应 输入被阻塞卡顿 输入始终流畅
Tab 切换时 loading 状态 整页 loading 保持旧内容,逐步显示
服务端首屏渲染 等待完整 HTML 流式返回,边加载边显示

为什么重要: 在 AI 聊天应用(如对话式 UI)中,用户输入必须即时响应,即使后台正在渲染大量消息历史,并发渲染确保了 UI 的流畅性。


1.3 组件库设计

题目

设计一个可定制的通用 Button 组件,需要支持:

  • 多种变体(primary/secondary/danger)
  • 多种尺寸(small/medium/large)
  • 加载状态
  • 图标支持(左侧/右侧)
  • 禁用状态
  • 遵循开闭原则,便于扩展

参考答案

tsx 复制代码
import React, { forwardRef } from 'react';
import { Spinner } from './Spinner';
import './Button.css';

// Props 类型定义
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
type ButtonSize = 'small' | 'medium' | 'large';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;        // 变体
  size?: ButtonSize;               // 尺寸
  loading?: boolean;              // 加载状态
  leftIcon?: React.ReactNode;      // 左侧图标
  rightIcon?: React.ReactNode;     // 右侧图标
  fullWidth?: boolean;             // 全宽
}

// 复合组件模式 - 扩展性强
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
  children,
  variant = 'primary',
  size = 'medium',
  loading = false,
  leftIcon,
  rightIcon,
  fullWidth = false,
  disabled,
  className = '',
  ...props
}, ref) => {
  const isDisabled = disabled || loading;

  return (
    <button
      ref={ref}
      className={`btn btn--${variant} btn--${size} ${fullWidth ? 'btn--full' : ''} ${className}`}
      disabled={isDisabled}
      {...props}
    >
      {/* 加载状态 */}
      {loading && (
        <span className="btn__spinner">
          <Spinner size="small" />
        </span>
      )}

      {/* 图标 + 文字 */}
      {!loading && leftIcon && <span className="btn__icon btn__icon--left">{leftIcon}</span>}
      <span className="btn__text">{children}</span>
      {!loading && rightIcon && <span className="btn__icon btn__icon--right">{rightIcon}</span>}
    </button>
  );
});

Button.displayName = 'Button';

// 导出按钮组复合组件
Button.Group = ({ children, className = '' }) => (
  <div className={`btn-group ${className}`}>{children}</div>
);

export { Button };

CSS 变量扩展方式:

css 复制代码
/* 使用 CSS 变量便于主题定制 */
.btn {
  --btn-bg: var(--color-primary);
  --btn-text: var(--color-white);
  --btn-padding: var(--spacing-md);

  background: var(--btn-bg);
  color: var(--btn-text);
  padding: var(--btn-padding);
  border-radius: 6px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* 变体扩展 - 新增变体无需修改组件代码 */
.btn--secondary {
  --btn-bg: transparent;
  --btn-text: var(--color-primary);
  border: 1px solid var(--color-primary);
}

详细解析

设计要点:

  1. forwardRef: 暴露 DOM 引用,便于父组件获取按钮元素做聚焦等操作。

  2. 复合组件模式 : Button.Group 挂在 Button 上,形成 API 家族,使用时 <Button.Group><Button /></Button.Group>

  3. CSS 变量: 将颜色、间距等设计 token 抽离为 CSS 变量,支持主题定制,无需修改组件代码。

  4. Props 扩展 : 使用 ...props 传递原生 button 属性(onClick、type、aria-* 等),保持原生语义。

  5. 开闭原则: 新增变体只需添加 CSS 类,不改 TSX;新增功能(如 Tooltip 包装)可通过 composition 实现。


1.4 conversation UI

题目

在 AI 对话应用(如 ChatGPT、Claude)中,设计一个支持 流式输出(Streaming) 的消息展示组件,需要考虑:

  • 打字机效果(逐字显示)
  • Markdown 渲染(支持代码高亮)
  • 消息复制功能
  • 加载中断功能
  • 思考过程展示(如 Claude 的 thinking)

参考答案

tsx 复制代码
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Copy, Square, Loader } from 'lucide-react';

interface MessageProps {
  role: 'user' | 'assistant';
  content: string;
  isStreaming?: boolean;
  isThinking?: boolean;
  thinkingContent?: string;
  onCancel?: () => void;
}

function Message({ role, content, isStreaming, isThinking, thinkingContent, onCancel }: MessageProps) {
  const [displayedContent, setDisplayedContent] = useState('');
  const [copied, setCopied] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);

  // 流式更新:当 content 变化时,逐步更新显示内容
  useEffect(() => {
    if (!isStreaming) {
      setDisplayedContent(content);
      return;
    }

    // 模拟打字机效果
    let index = 0;
    const timer = setInterval(() => {
      setDisplayedContent(content.slice(0, index + 1));
      index++;
      if (index >= content.length) clearInterval(timer);
    }, 20);

    return () => clearInterval(timer);
  }, [content, isStreaming]);

  // 自动滚动到底部
  useEffect(() => {
    containerRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [displayedContent]);

  const handleCopy = async () => {
    await navigator.clipboard.writeText(content);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div className={`message message--${role}`} ref={containerRef}>
      <div className="message__content">
        {/* Markdown 渲染 */}
        <ReactMarkdown
          components={{
            code({ node, className, children, ...props }) {
              const match = /language-(\w+)/.exec(className || '');
              return !isStreaming && match ? (
                <SyntaxHighlighter language={match[1]} {...props}>
                  {String(children).replace(/\n$/, '')}
                </SyntaxHighlighter>
              ) : (
                <code className={className} {...props}>{children}</code>
              );
            }
          }}
        >
          {displayedContent}
        </ReactMarkdown>

        {/* 思考过程(流式输出时不显示) */}
        {!isStreaming && isThinking && thinkingContent && (
          <details className="message__thinking">
            <summary>Thinking...</summary>
            <ReactMarkdown>{thinkingContent}</ReactMarkdown>
          </details>
        )}
      </div>

      {/* 操作按钮 */}
      {!isStreaming && (
        <div className="message__actions">
          <button onClick={handleCopy} title="复制">
            {copied ? '已复制' : <Copy size={14} />}
          </button>
        </div>
      )}

      {/* 流式加载控制 */}
      {isStreaming && (
        <button className="message__stop" onClick={onCancel} title="停止生成">
          <Square size={14} /> 停止
        </button>
      )}

      {isStreaming && (
        <span className="message__streaming-indicator">
          <Loader className="spin" size={14} />
        </span>
      )}
    </div>
  );
}

export { Message };

详细解析

关键技术点:

  1. 流式数据处理 : 使用 useEffect 监听 content 变化,通过 setInterval 模拟打字机效果。生产环境通常通过 EventSourceReadableStream 接收 SSE 数据。

  2. Markdown 渲染 : 使用 react-markdown + react-syntax-highlighter 处理代码高亮,流式输出时暂不渲染代码块避免闪烁。

  3. 滚动管理 : scrollIntoView({ behavior: 'smooth' }) 确保新消息自动滚动到视口内。

  4. 思考过程 : Claude 等模型的 thinking 内容通过 <details> 折叠,支持展开/收起。

  5. 中断生成 : 提供 onCancel 回调,实际实现中需要中断 fetch 请求或 abort controller。


二、JavaScript/TypeScript (T1)

2.1 event loop

题目

请分析以下代码的执行顺序,并说明 JavaScript 事件循环(Event Loop)的工作机制:

javascript 复制代码
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

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

console.log('6');

// 请问输出顺序是什么?为什么?

参考答案

输出顺序 : 163524

执行过程分析:

步骤 操作 说明
1 console.log('1') 同步代码,直接执行
2 setTimeout(() => console.log('2'), 0) 宏任务,加入 macrotask queue
3 Promise.resolve().then() 微任务,加入 microtask queue
4 Promise.resolve().then() 微任务,加入 microtask queue
5 console.log('6') 同步代码,直接执行
6 检查微任务队列 依次执行: 35
7 setTimeout(() => console.log('4'), 0) 宏任务,加入队列
8 检查宏任务队列 执行: 24

详细解析

Event Loop 三层结构:

javascript 复制代码
┌─────────────────────────────────────────────────────┐
│                    Call Stack                        │
│         (同步代码执行在此)                            │
└─────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────┐
│                  Microtask Queue                     │
│     Promise.then / MutationObserver / queueMicrotask │
│     ✓ 每次循环清空所有微任务                          │
└─────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────┐
│                  Macrotask Queue                     │
│       setTimeout / setInterval / I/O / UI render     │
│       每次循环执行一个宏任务                          │
└─────────────────────────────────────────────────────┘

核心规则:

  1. 同步代码先执行
  2. 所有微任务执行完毕后,才执行一个宏任务
  3. 微任务可以添加新的微任务,新的微任务会在当前微任务队列清空前执行

关键理解 : setTimeout(..., 0) 并不保证立即执行,只是将回调放入下一个宏任务队列的队首。


2.2 promise

题目

请手写一个 Promise.myAll 方法,实现类似 Promise.all 的功能,并处理以下边界情况:

  • 所有 Promise 都成功
  • 任意一个 Promise 失败
  • 传入空数组
  • 包含非 Promise 值

参考答案

javascript 复制代码
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    // 边界情况:空数组直接返回
    if (!promises || promises.length === 0) {
      resolve([]);
      return;
    }

    const results = new Array(promises.length);
    let completedCount = 0;
    const total = promises.length;

    promises.forEach((promise, index) => {
      // 使用 Promise.resolve 处理非 Promise 值
      Promise.resolve(promise).then(
        (value) => {
          results[index] = value;
          completedCount++;

          // 所有都完成后返回结果数组
          if (completedCount === total) {
            resolve(results);
          }
        },
        (error) => {
          // 任意一个失败,立即 reject
          reject(error);
        }
      );
    });
  });
};

// 测试用例
Promise.myAll([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(console.log); // [1, 2, 3]

Promise.myAll([
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3)
]).catch(console.error); // 'error'

Promise.myAll([]).then(console.log); // []

Promise.myAll([1, 2, 3]).then(console.log); // [1, 2, 3] (自动包装)

详细解析

核心实现逻辑:

  1. Promise.resolve() 包装 : 确保传入的值无论是 Promise 还是普通值,都能用 .then() 处理。

  2. 计数器机制 : completedCount 记录已完成数量,用于判断是否全部完成。

  3. 结果数组位置固定 : 使用 results[index] = value 确保结果顺序与输入顺序一致,而非按完成顺序。

  4. 短路失败 : 一旦任意 Promise reject,立即调用 reject(),不会等待其他 Promise。

与原生 Promise.all 的区别:

  • 原生 Promise.all 在 reject 后会取消剩余 Promise 的执行(但实际已在运行)
  • 手写版可在此基础上添加 Promise.race 竞速机制优化

2.3 闭包

题目

请解释以下代码的输出结果,并说明 JavaScript 闭包的工作原理:

javascript 复制代码
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

参考答案

输出结果:

  • 第一个循环: 3, 3, 3
  • 第二个循环: 0, 1, 2

执行过程解析:

var 版本(闭包陷阱):

javascript 复制代码
// 实际等效于:
var i;  // 变量提升到函数作用域
for (i = 0; i < 3; i++) {
  // setTimeout 闭包捕获的是同一个变量 i
  setTimeout(() => console.log(i), 100);
}
// 100ms 后,setTimeout 回调执行时,i 已经是 3

let 版本(块级作用域):

javascript 复制代码
// let 创建块级作用域,每次迭代都有独立的 i
for (let i = 0; i < 3; i++) {
  // 每次迭代创建新的作用域,闭包捕获的是该作用域的 i
  setTimeout(() => console.log(i), 100);
}
// 每个 setTimeout 捕获的 i 分别是 0, 1, 2

修复 var 版本的几种方式:

javascript 复制代码
// 方法1:使用 IIFE 创建新作用域
for (var i = 0; i < 3; i++) {
  ((index) => {
    setTimeout(() => console.log(index), 100);
  })(i);
}

// 方法2:使用 setTimeout 的第三个参数
for (var i = 0; i < 3; i++) {
  setTimeout((index) => console.log(index), 100, i);
}

// 方法3:封装为函数
const log = (index) => setTimeout(() => console.log(index), 100);
for (var i = 0; i < 3; i++) {
  log(i);
}

详细解析

闭包的核心原理:

  1. 词法作用域 (Lexical Scope): 函数在定义时决定其作用域,而非调用时。

  2. 闭包 = 函数 + 词法环境的引用: 闭包让函数能够访问其外部作用域的变量,即使外部函数已执行完毕。

  3. var vs let:

    • var 是函数作用域,循环结束后变量仍然存在
    • let 是块级作用域,每次迭代创建独立绑定
  4. 内存泄漏注意: 闭包会持有外部变量的引用,可能导致不需要的变量无法被 GC 回收。


2.4 TS泛型

题目

实现一个类型安全的 pick 函数,从对象中选取指定属性:

typescript 复制代码
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// 实现后能这样使用:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  // TODO: 实现
}

// 使用示例
const user: User = { id: 1, name: 'Tom', email: 'tom@test.com', age: 25 };
const result = pick(user, ['id', 'name']);
// result 类型应为: { id: number; name: string; }

参考答案

typescript 复制代码
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// 核心实现
function pick<T extends object, K extends keyof T>(
  obj: T,
  keys: K[]
): { [P in K]: T[P] } {
  const result = {} as { [P in K]: T[P] };
  keys.forEach(key => {
    if (key in obj) {
      result[key] = obj[key];
    }
  });
  return result;
}

// 使用示例
const user: User = { id: 1, name: 'Tom', email: 'tom@test.com', age: 25 };

// 类型推导正确
const result1 = pick(user, ['id', 'name']);
// result1: { id: number; name: string; }

// 错误使用:编译器报错
const result2 = pick(user, ['id', 'invalid']);  // ❌ TypeScript Error

// 返回值可直接使用,类型安全
result1.id;   // ✅ number
result1.name; // ✅ string
result1.email; // ❌ Property 'email' does not exist

// 进阶:添加可选参数的版本
function pickOptional<T extends object, K extends keyof T>(
  obj: T,
  keys: K[]
): Partial<Pick<T, K>> {
  return keys.reduce((acc, key) => {
    if (key in obj) {
      acc[key] = obj[key];
    }
    return acc;
  }, {} as Partial<Pick<T, K>>);
}

详细解析

泛型约束解析:

约束 含义
T extends object T 必须是对象类型(非原始值)
K extends keyof T K 必须是 T 的键名之一
{ [P in K]: T[P] } 映射类型,遍历 K 生成新类型

keyof 运算符: 取出对象类型的所有键名组成联合类型。

映射类型语法:

  • { [P in K]: T[P] } = { id: T['id'], name: T['name'] } = { id: number, name: string }

实际应用场景:

  • API 响应数据筛选
  • 表单提交字段控制
  • 状态管理中提取特定字段

三、Next.js (T1)

3.1 App Router

题目

对比 Next.js 的 Pages RouterApp Router,说明 App Router 的核心优势和适用场景。

参考答案

核心对比:

特性 Pages Router App Router
渲染方式 服务端/客户端分离 服务端组件默认
布局系统 _app.js / _document.js 嵌套 layout.tsx
导航 useRouter useRouter + Link
数据获取 getServerSideProps async Server Component
路由组 (group) (group)
加载状态 - loading.tsx
错误处理 error.js error.tsx

App Router 核心优势:

tsx 复制代码
// app/posts/[id]/page.tsx
// 这是一个 Server Component(默认)
// 无需 useEffect / useState,直接使用 async/await

import { db } from '@/lib/db';
import { notFound } from 'next/navigation';

// 服务端直接获取数据
async function getPost(id: string) {
  const post = await db.post.findUnique({ where: { id } });
  if (!post) notFound();  // 内置 notFound 处理
  return post;
}

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await getPost(params.id);

  return (
    <article>
      <h1>{post.title}</h1>
      {/* 直接渲染,无需等待 API */}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

嵌套布局示例:

less 复制代码
app/
├── layout.tsx          // 根布局(导航栏、Footer)
├── page.tsx           // 首页
├── posts/
│   ├── layout.tsx     // 嵌套布局(侧边栏)
│   ├── page.tsx       // 文章列表
│   └── [id]/
│       ├── page.tsx   // 文章详情
│       └── loading.tsx // 加载状态
└── loading.tsx        // 根级加载状态

详细解析

App Router 的核心优势:

  1. React Server Components (RSC): 默认服务端组件,减少客户端 JS bundle 大小,数据直接在服务端获取。

  2. 嵌套布局: 布局文件自动嵌套,无需手动管理组件状态共享。

  3. Streaming SSR : 结合 <Suspense> 实现流式渲染,首屏先显示骨架屏,内容逐步加载。

  4. 更细粒度的加载/错误处理 : 每个路由段可独立配置 loading.tsxerror.tsx

  5. Server Actions: 服务端函数可直接调用,简化表单处理和数据变更。


3.2 SSR

题目

解释 Next.js 中的 SSR(服务端渲染)CSR(客户端渲染) 的区别,以及如何选择渲染策略。

参考答案

渲染策略对比:

策略 首屏速度 SEO 交互性 服务器压力
SSR 慢(需水合)
CSR
SSG 最快 慢(需水合) 无(构建时生成)
ISR

Next.js 渲染策略选择:

tsx 复制代码
// 1. 静态生成 (SSG) - 适合内容固定不变的页面
// 编译时生成,后续请求直接返回缓存

// app/about/page.tsx
export const dynamic = 'force-static'; // 或缺失此行,默认 SSG

// 2. ISR - 适合内容频繁变化但不需要实时数据的页面
// 增量重新生成,定期更新缓存
export const revalidate = 60; // 每 60 秒重新生成一次

async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 } // 单个 API 请求的缓存策略
  });
  return res.json();
}

// 3. SSR - 适合需要实时数据或个性化内容的页面
export const dynamic = 'force-dynamic';

async function getUserProfile(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    cache: 'no-store' // 不缓存,实时请求
  });
  return res.json();
}

// 4. 客户端渲染 - 适合高度交互的组件
'use client';

import { useEffect, useState } from 'react';

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

  useEffect(() => {
    // 客户端获取数据
    fetch('/api/dashboard').then(res => res.json()).then(setData);
  }, []);

  if (!data) return <Skeleton />;
  return <div>{data.content}</div>;
}

详细解析

选择建议:

  1. 静态内容(文档、博客、营销页): 使用 SSG,构建时生成,首屏最快。

  2. 动态但变化不频繁(电商列表、新闻): 使用 ISR,定期增量更新。

  3. 个性化数据(用户主页、仪表盘): 使用 SSR,每次请求实时获取。

  4. 高度交互(聊天、游戏、实时协作): CSR + 客户端数据获取。

App Router 中的新选择:

  • React Server Components: 默认服务端渲染,无水合开销,更好的性能
  • loading.tsx: 配合 Suspense 实现更好的加载体验

3.3 Server Component

题目

在 Next.js App Router 中,Server ComponentsClient Components 的界限如何划分?各自适用什么场景?

参考答案

组件类型划分:

tsx 复制代码
// ============================================
// Server Components(默认,无需声明)
// ============================================
// - 直接访问数据库、文件系统
// - 使用 secrets/API keys(不暴露给客户端)
// - 减少客户端 JavaScript bundle
// - 无限执行时间
// - 不能使用: useState, useEffect, 事件监听

// app/page.tsx - Server Component(默认)
import { db } from '@/lib/db';
import ClientComponent from './ClientComponent'; // 导入客户端组件

export default async function Page() {
  // 直接访问数据库,无需 API 路由
  const posts = await db.post.findMany({
    where: { published: true },
    take: 10
  });

  return (
    <div>
      <h1>Latest Posts</h1>
      {/* 传递数据给客户端组件 */}
      <ClientComponent initialPosts={posts} />
    </div>
  );
}

// ============================================
// Client Components(必须声明 'use client')
// ============================================
// - 需要交互(onClick, onChange)
// - 需要状态(useState, useReducer)
// - 需要生命周期(useEffect)
// - 使用浏览器 API

// components/Dashboard.tsx
'use client';

import { useState, useEffect } from 'react';

interface Post {
  id: number;
  title: string;
}

export function Dashboard({ initialPosts }: { initialPosts: Post[] }) {
  const [filter, setFilter] = useState('');

  // 客户端过滤,无需重新请求服务器
  const filteredPosts = initialPosts.filter(p =>
    p.title.includes(filter)
  );

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter posts..."
      />
      {filteredPosts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

边界设计原则:

arduino 复制代码
Server Component          Server Component
     │                          │
     │    ┌── props (可序列化) ──┤
     │    │                      │
     ▼    ▼                      ▼
┌─────────────────────────────────────┐
│          Client Component            │
│  'use client'                        │
│  - 状态、交互、副作用                 │
└─────────────────────────────────────┘

详细解析

关键规则:

  1. 只能向下传递: Server Component 可以导入 Client Component,但 Client Component 不能导入 Server Component(可以 props 接收)。

  2. Props 必须可序列化: 传递给 Client Component 的 props 必须是 JSON 可序列化的。

  3. 组合策略: 在 Server Component 中组合多个 Client Component,每个 Client Component 独立水合。

  4. 性能收益: Server Components 不增加客户端 JS bundle,交互少的页面几乎没有客户端代码。


3.4 streaming

题目

解释 Next.js App Router 中的 Streaming SSR 机制,以及它如何提升用户体验?

参考答案

Streaming SSR 工作原理:

tsx 复制代码
// app/blog/[slug]/page.tsx
import { Suspense } from 'react';
import { getPost, getRelatedPosts } from '@/lib/posts';

// 博客内容 - 较慢的数据库查询
async function PostContent({ slug }: { slug: string }) {
  const post = await getPost(slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <div className="prose">{post.content}</div>
    </article>
  );
}

// 相关推荐 - 快速的缓存查询
async function RelatedPosts({ tags }: { tags: string[] }) {
  const posts = await getRelatedPosts(tags);
  return (
    <aside>
      <h3>Related Posts</h3>
      {posts.map(p => <Link key={p.id} href={`/blog/${p.id}`}>{p.title}</Link>)}
    </aside>
  );
}

export default async function BlogPage({ params }: { params: { slug: string } }) {
  return (
    <main>
      {/* 内容较慢,使用 Suspense 包裹 */}
      <Suspense fallback={<PostSkeleton />}>
        <PostContent slug={params.slug} />
      </Suspense>

      {/* 推荐较快,独立流式传输 */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedPosts tags={['tech']} />
      </Suspense>
    </main>
  );
}

加载骨架屏:

tsx 复制代码
# app/blog/[slug]/PostSkeleton.tsx
export function PostSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/2 mb-4" />
      <div className="space-y-2">
        <div className="h-4 bg-gray-200 rounded" />
        <div className="h-4 bg-gray-200 rounded w-5/6" />
        <div className="h-4 bg-gray-200 rounded w-4/6" />
      </div>
    </div>
  );
}

渲染时序:

ini 复制代码
t=0ms    ├─────────────────────┬─────────────────────┐
         │  HTML Shell         │                     │
         │  (立即返回)          │                     │
t=100ms  ├─────────────────────┼─────────────────────┤
         │  PostSkeleton       │  RelatedPosts       │
         │  (展开)             │  (立即完成)         │
t=500ms  ├─────────────────────┼─────────────────────┤
         │  PostContent       │                     │
         │  (数据返回后渲染)    │                     │
t=600ms  └─────────────────────┴─────────────────────┘

详细解析

Streaming 的优势:

  1. TTFB (Time to First Byte) 更短: HTML shell 立即返回,无需等待所有数据就绪。

  2. 并行数据获取: 多个 Suspense 边界可以并行获取数据。

  3. 分级加载体验: 快的部分先展示,慢的部分用骨架屏过渡。

  4. 用户体验提升: 用户感知到的加载时间大幅缩短,减少跳出率。

适用场景:

  • 博客文章(正文慢,侧边栏快)
  • 产品详情(描述慢,评价快)
  • 仪表盘(图表慢,概览快)

四、前端工程化 (T1)

4.1 webpack/vite

题目

对比 WebpackVite 的构建原理,说明为什么 Vite 在开发环境具有显著的性能优势,以及它们各自的适用场景。

参考答案

核心对比:

维度 Webpack Vite
开发服务器 DevServer + 打包构建 原生 ESM + esbuild
热更新 重新打包相关模块 按需更新(HMR)
生产构建 webpack + terser Rollup
启动速度 O(n) 随模块数增长 O(1) 恒定时间
缓存 持久化缓存需配置 自动基于缓存

Webpack 工作原理:

javascript 复制代码
// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name].[contenthash].js',
    path: '/dist'
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        }
      }
    }
  }
};

Vite 工作原理:

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    // 生产构建使用 Rollup
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom']
        }
      }
    }
  },
  optimizeDeps: {
    // 依赖预构建
    include: ['react', 'react-dom', 'lodash']
  }
});

详细解析

Vite 性能优势原理:

arduino 复制代码
┌─────────────────────────────────────────────────────┐
│                    Webpack Dev Server                │
│                                                      │
│   修改文件 → 重新构建整个模块依赖图 → 重新打包 → 发送 │
│   慢:每次修改都需要完整构建                            │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│                    Vite Dev Server                   │
│                                                      │
│   启动:esbuild 预构建依赖(Node_modules 打包)        │
│   运行:浏览器直接请求源文件(原生 ESM)               │
│   修改:仅重新请求单个修改文件 + HMR                   │
│   快:无需打包,按需加载                              │
└─────────────────────────────────────────────────────┘

Vite 适用场景:

  • 新项目、中小型项目(开发体验优先)
  • React/Vue/Svelte 等现代框架
  • 需要快速启动的 CI/CD 流程

Webpack 适用场景:

  • 大型复杂项目(有完善的持久化缓存配置)
  • 需要精细控制构建流程
  • 老项目迁移(生态丰富)

4.2 微前端

题目

解释 微前端(Micro Frontends) 的概念和实现方式,并以 Qiankun 为例说明子应用接入的关键步骤。

参考答案

微前端核心思想: 将前端应用拆分为多个独立可部署的子应用,每个子应用可以由不同团队使用不同技术栈开发。

qiankun 接入示例:

javascript 复制代码
// 1. 主应用配置 (main-app/src/main.js)
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

registerMicroApps([
  {
    name: 'react-sub',                    // 子应用名称
    entry: '//localhost:3001',             // 子应用入口
    container: '#sub-app-container',     // 挂载容器
    activeRule: '/react',                 // 激活路由
    props: {                              // 传递给子应用的数据
      mainRouter: history
    }
  },
  {
    name: 'vue-sub',
    entry: '//localhost:3002',
    container: '#sub-app-container',
    activeRule: '/vue'
  }
]);

// 设置默认进入的子应用
setDefaultMountApp('/react');

// 启动微前端
start({
  prefetch: 'all',  // 预加载策略
  sandbox: {
    strictStyleIsolation: true  // 样式隔离
  }
});

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
javascript 复制代码
// 2. React 子应用配置 (react-sub/src/main.js)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

/**
 * 导出生命周期函数供主应用调用
 */
let history;

export async function bootstrap() {
  console.log('React 子应用 bootstrap');
}

export async function mount(props) {
  console.log('React 子应用 mount', props);
  history = props.mainRouter;

  ReactDOM.createRoot(document.getElementById('root')).render(
    <App />
  );
}

export async function unmount() {
  console.log('React 子应用 unmount');
  // 清理工作
}
javascript 复制代码
// 3. 子应用 webpack 配置 (react-sub/webpack.config.js)
const { name } = require('./package.json');

module.exports = {
  output: {
    library: `${name}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${name}`
  }
};

详细解析

微前端的价值:

优势 说明
技术栈无关 各子应用可使用 React/Vue/Angular
独立开发 团队独立迭代,无代码冲突
独立部署 子应用单独发布,快速迭代
增量升级 逐步替换旧模块,降低风险

挑战与解决方案:

  1. 样式冲突: 使用 CSS Modules 或 Shadow DOM 隔离
  2. 状态共享 : 使用 initGlobalState 或 Event Bus
  3. 公共依赖: 提取为 shared chunk,主应用加载
  4. 路由管理: 主应用统一管理,子应用监听路由变化

4.3 monorepo

题目

什么是 Monorepo?使用 Monorepo 管理前端项目有哪些优势和挑战?请以 pnpm workspace 为例说明配置方法。

参考答案

Monorepo 核心概念: 将多个相关项目(packages)放在同一个代码仓库中统一管理。

pnpm workspace 配置:

yaml 复制代码
# pnpm-workspace.yaml
packages:
  - 'packages/*'      # 所有 packages 目录下的包
  - 'apps/*'          # 所有应用
  - '!packages/docs' # 排除特定目录

项目结构:

csharp 复制代码
my-monorepo/
├── pnpm-workspace.yaml    # workspace 配置
├── pnpm-lock.yaml
├── .eslintrc.js
├── tsconfig.base.json     # 基础 TypeScript 配置
│
├── apps/
│   ├── web/               # 主应用
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── admin/             # 管理后台
│
└── packages/
    ├── ui/                # UI 组件库
    │   ├── package.json
    │   └── src/
    │       ├── Button/
    │       └── Input/
    ├── utils/             # 工具函数库
    └── shared/            # 共享类型/常量
json 复制代码
// apps/web/package.json
{
  "name": "@my-app/web",
  "version": "1.0.0",
  "dependencies": {
    "@my-app/ui": "workspace:*",  // 使用 workspace 协议
    "@my-app/utils": "workspace:*",
    "react": "^18.0.0"
  }
}
bash 复制代码
# 安装依赖 - 自动链接 workspace 包
pnpm install

# 安装到根目录
pnpm add -w -D eslint

# 安装到指定包
pnpm --filter @my-app/web add react-router-dom

# 构建所有包(依赖顺序)
pnpm run -r build

详细解析

Monorepo 优势:

优势 说明
代码复用 多个项目共享组件、工具
一致性 统一依赖版本,避免版本碎片
原子提交 跨包改动可一次性提交
简化依赖管理 统一的 node_modules

挑战与解决方案:

  1. 构建性能 : 使用 turbonx 增量构建,只构建受影响的包
  2. CI/CD : 配置 turbo.json 跳过无变更包的构建
  3. 代码规范: 统一 ESLint/Prettier 配置到根目录
  4. 版本管理 : 使用 changesets 管理包版本和 changelog

4.4 pnpm/turbo

题目

组合使用 pnpm + Turbo 构建高性能的 Monorepo 工具链,请说明配置方法和增量构建原理。

参考答案

Turbo 配置文件:

json 复制代码
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],  // ^ 表示依赖的包先构建
      "outputs": ["dist/**", ".next/**"]  // 缓存输出
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**", "*.config.js"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,  // 开发模式不缓存
      "persistent": true  // 常驻进程
    }
  }
}

package.json 脚本:

json 复制代码
// apps/web/package.json
{
  "scripts": {
    "dev": "turbo run dev --filter=@my-app/web",
    "build": "turbo run build --filter=@my-app/web",
    "lint": "turbo run lint",
    "test": "turbo run test"
  }
}

增量构建原理:

bash 复制代码
修改 packages/ui/Button/index.tsx
         ↓
Turbo 检测依赖图
         ↓
┌─────────────────────────────────────┐
│ 受影响包分析:                        │
│ - @my-app/ui → 需要重新构建 ✓        │
│ - @my-app/web → 依赖 ui,需要构建 ✓   │
│ - @my-app/admin → 不受影响,跳过 ✓   │
└─────────────────────────────────────┘
         ↓
使用本地/远程缓存恢复未变更输出
         ↓
实际只构建 @my-app/ui 和 @my-app/web

Remote Cache(可选):

bash 复制代码
# 使用 Vercel Remote Cache 或自建
TURBO_TOKEN=xxx TURBO_TEAM=my-team pnpm build

详细解析

Turbo 核心特性:

  1. 任务调度 : 根据 pipeline 定义的任务依赖和顺序执行
  2. 远程缓存: 相同的输入产生相同的输出,可跨机器共享缓存
  3. 影响分析: 智能检测哪些包需要重新构建
  4. 并行执行: 无依赖的任务可并行执行

性能收益示例:

场景 无缓存构建 Turbo 缓存 节省时间
100 包全量构建 10 分钟 10 分钟 0
修改 1 个底层包 10 分钟 ~30 秒 ~95%
Pull Request CI 10 分钟 ~1 分钟 ~90%

五、前端性能优化 (T1)

5.1 懒加载

题目

在现代前端应用中,有哪些懒加载的实现方式?请分别说明路由懒加载、组件懒加载和资源懒加载的实现方法。

参考答案

1. 路由懒加载:

tsx 复制代码
// React Router v6
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </Suspense>
  );
}
tsx 复制代码
// Vue Router
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('./views/About.vue')
  }
];

2. 组件懒加载:

tsx 复制代码
// React - Suspense + lazy
const HeavyChart = lazy(() => import('./components/HeavyChart'));

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart data={chartData} />
      </Suspense>
    </div>
  );
}

// Vue - defineAsyncComponent
import { defineAsyncComponent } from 'vue';

const HeavyModal = defineAsyncComponent(() =>
  import('./components/HeavyModal.vue')
);

3. 图片懒加载:

tsx 复制代码
// Intersection Observer API
function LazyImage({ src, alt }) {
  const [isVisible, setIsVisible] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { rootMargin: '100px' }  // 提前 100px 开始加载
    );

    observer.observe(imgRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <img
      ref={imgRef}
      src={isVisible ? src : placeholder}
      alt={alt}
      loading="lazy"  // 原生属性作为降级方案
    />
  );
}

4. 动态导入非路由模块:

tsx 复制代码
// 根据条件动态加载
async function loadFeature() {
  if (user.isPro) {
    const { ProEditor } = await import('./ProEditor');
    return <ProEditor />;
  }
  return <BasicEditor />;
}

详细解析

懒加载的核心价值:

优化点 说明
首屏加载 减少初始 JS bundle 大小
TTI 用户可交互时间提前
带宽节省 按需加载,节省流量
缓存效率 非首屏代码独立缓存

最佳实践:

  1. 首屏关键路由必须同步加载
  2. 非首屏路由使用 React.lazy + Suspense
  3. 图片使用 loading="lazy" 或 Intersection Observer
  4. 大型库(如图表)使用动态 import

5.2 虚拟滚动

题目

实现一个高性能的 虚拟滚动列表,处理百万级数据渲染场景。请说明核心原理并实现关键代码。

参考答案

虚拟滚动核心原理:

makefile 复制代码
┌─────────────────────────────────────────┐
│              Viewport (可视区域)          │
│  ┌─────────────────────────────────┐    │
│  │  Item 15                        │    │
│  │  Item 16  ←── 可见区域            │    │
│  │  Item 17                        │    │
│  │  Item 18                        │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

只渲染: Item 15, 16, 17, 18(可能多渲染几个缓冲项)
总数据: 1,000,000 条

React 虚拟列表实现:

tsx 复制代码
import React, { useState, useEffect, useRef, useCallback } from 'react';

interface VirtualListProps<T> {
  items: T[];
  height: number;
  itemHeight: number;
  renderItem: (item: T, index: number) => React.ReactNode;
}

function VirtualList<T>({ items, height, itemHeight, renderItem }: VirtualListProps<T>) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);

  // 计算可见范围
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 2);
  const endIndex = Math.min(
    items.length - 1,
    Math.ceil((scrollTop + height) / itemHeight) + 2
  );

  // 只渲染可见区域的项
  const visibleItems = [];
  for (let i = startIndex; i <= endIndex; i++) {
    visibleItems.push(
      <div
        key={i}
        style={{
          position: 'absolute',
          top: `${i * itemHeight}px`,
          height: `${itemHeight}px`,
          left: 0,
          right: 0
        }}
      >
        {renderItem(items[i], i)}
      </div>
    );
  }

  const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop);
  }, []);

  // 总高度占位,保持滚动条正确
  const totalHeight = items.length * itemHeight;

  return (
    <div
      ref={containerRef}
      style={{ height: `${height}px`, overflow: 'auto', position: 'relative' }}
      onScroll={handleScroll}
    >
      {/* 占位元素 */}
      <div style={{ height: `${totalHeight}px`, position: 'relative' }}>
        {/* 可视内容 */}
        {visibleItems}
      </div>
    </div>
  );
}

// 使用示例
function App() {
  const items = Array.from({ length: 1_000_000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    email: `user${i}@example.com`
  }));

  return (
    <VirtualList
      items={items}
      height={600}
      itemHeight={50}
      renderItem={(item) => (
        <div className="flex items-center gap-4 p-2 border-b">
          <span className="w-20">{item.id}</span>
          <span>{item.name}</span>
          <span className="text-gray-500">{item.email}</span>
        </div>
      )}
    />
  );
}

详细解析

虚拟滚动关键点:

  1. 固定高度假设: 为了简化计算,实际使用中列表项高度通常需要固定或预知。

  2. 缓冲区域 : ±2 缓冲项防止快速滚动时出现空白。

  3. 滚动监听 : 使用 onScrolladdEventListener 监听滚动事件。

  4. 位置计算 : 使用 transform: translateY() 替代 top 可获得更好的重绘性能。

进阶优化:

  • 使用 will-change: transform 提示浏览器优化
  • 对滚动事件使用 throttle/debouncerequestAnimationFrame
  • 考虑使用 @tanstack/react-virtual 等成熟库

5.3 长列表优化

题目

针对一个新闻 Feed 流场景,请设计完整的性能优化方案,包括首屏加载、数据分页、滚动优化等。

参考答案

完整 Feed 流优化方案:

tsx 复制代码
// 1. 无限滚动 + 分页
function Feed() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);
  const observerRef = useRef();

  // 使用 Intersection Observer 触发加载
  const lastPostRef = useCallback(node => {
    if (loading) return;
    if (observerRef.current) observerRef.current.disconnect();

    observerRef.current = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting && hasMore) {
        setPage(p => p + 1);
      }
    });
    if (node) observerRef.current.observe(node);
  }, [loading, hasMore]);

  // 数据获取
  useEffect(() => {
    async function fetchPosts() {
      setLoading(true);
      const data = await api.getPosts({ page, limit: 20 });
      setPosts(prev => [...prev, ...data.posts]);
      setHasMore(data.hasMore);
      setLoading(false);
    }
    fetchPosts();
  }, [page]);

  return (
    <div className="feed">
      {posts.map((post, index) => (
        <PostCard
          key={post.id}
          post={post}
          ref={index === posts.length - 1 ? lastPostRef : null}
        />
      ))}
      {loading && <LoadingSpinner />}
      {!hasMore && <EndOfFeed />}
    </div>
  );
}

// 2. 图片懒加载 + 渐进加载
function PostImage({ src, alt }) {
  const [loaded, setLoaded] = useState(false);
  const [error, setError] = useState(false);

  return (
    <div className="relative bg-gray-200">
      {/* 低质量占位图 (BlurHash/LQIP) */}
      {!loaded && <div className="absolute inset-0 bg-gray-300 animate-pulse" />}

      <img
        src={error ? fallbackSrc : src}
        alt={alt}
        loading="lazy"
        onLoad={() => setLoaded(true)}
        onError={() => setError(true)}
        className={`transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'}`}
      />
    </div>
  );
}

// 3. 时间分桶 - 减少渲染节点
function GroupedFeed({ posts }) {
  // 按日期分组
  const grouped = posts.reduce((acc, post) => {
    const date = dayjs(post.createdAt).format('YYYY-MM-DD');
    (acc[date] || (acc[date] = [])).push(post);
    return acc;
  }, {});

  return (
    <div>
      {Object.entries(grouped).map(([date, datePosts]) => (
        <div key={date}>
          <DateHeader date={date} />
          {datePosts.map(post => (
            <PostCard key={post.id} post={post} />
          ))}
        </div>
      ))}
    </div>
  );
}

// 4. 虚拟滚动适配
function VirtualizedFeed({ posts }) {
  return (
    <VirtualList
      items={posts}
      height={window.innerHeight}
      itemHeight={estimatedHeight} // 估算高度
      renderItem={(post) => <PostCard post={post} />}
      overscan={5}
    />
  );
}

详细解析

Feed 流优化矩阵:

优化点 技术方案 收益
首屏加载 Skeleton + 预加载 感知加载时间↓
分页 Cursor/Offset 分页 内存占用↓
无限滚动 Intersection Observer 自动化加载
图片 LQIP + Lazy Load 首屏流量↓, FCP↑
渲染 时间分桶/虚拟列表 渲染节点↓
缓存 SWR/React Query 重复请求↓

关键指标监控:

javascript 复制代码
// 监控 Feed 性能
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log({
      type: entry.name,
      duration: entry.duration,
      size: entry.transferSize
    });
  }
});

observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input'] });

5.4 performance分析

题目

如何使用 Chrome DevTools 进行前端性能分析?请说明 Performance 面板的核心指标和分析方法。

参考答案

Performance 面板分析流程:

css 复制代码
┌────────────────────────────────────────────────────────────┐
│                    Performance 面板                        │
├────────────────────────────────────────────────────────────┤
│ ┌─ 控制栏 ─────────────────────────────────────────────┐  │
│ │ [●录制] [⟳ 重新加载] [清除]     Screenshots ▼  ▼ 3G   │  │
│ └───────────────────────────────────────────────────────┘  │
│ ┌─ 概览区 ─────────────────────────────────────────────┐  │
│ │ FPS ─────────────────────────────────────────────── │  │
│ │ CPU ─────────────────────────────────────────────── │  │
│ │ NET ─────────────────────────────────────────────── │  │
│ └───────────────────────────────────────────────────────┘  │
│ ┌─ 火焰图 ────────────────────────────────────────────┐  │
│ │ ████████████████  Main Thread (V8 JS)              │  │
│ │   └─ JS Evaluate ── Function Call ── GC ── Layout  │  │
│ └───────────────────────────────────────────────────────┘  │
│ ┌─ 网络时间线 ────────────────────────────────────────┐  │
│ │ [===html===][===css===][===js===][===img===]       │  │
│ └───────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘

核心指标解读:

指标 含义 优化目标
FP (First Paint) 首次绘制 < 1.8s
FCP (First Contentful Paint) 首次内容绘制 < 1.8s
LCP (Largest Contentful Paint) 最大内容绘制 < 2.5s
CLS (Cumulative Layout Shift) 布局偏移 < 0.1
TTI (Time to Interactive) 可交互时间 < 3.8s

火焰图分析方法:

javascript 复制代码
// 1. 识别长任务 (Long Tasks > 50ms)
Long Task ████████████████████████████████
          └─ 超过 50ms 的主线程任务

// 2. 定位卡顿原因
Main Thread:
  └─ Task: 200ms
     └─ Function: heavyCalculation (50ms)
     └─ Function: renderComponent (30ms)
     └─ Function: setState (120ms)  ← 状态更新触发大量重渲染
        └─ Component.render (80ms)
        └─ ChildComponent.render (40ms)

// 3. 优化策略
// - 拆分长任务为多个小任务
// - 使用 requestIdleCallback
// - 减少不必要的 setState

代码级性能分析:

javascript 复制代码
// 使用 Performance API 精确测量
performance.mark('operation-start');

// ... 执行耗时操作 ...

performance.mark('operation-end');
performance.measure('operation duration', 'operation-start', 'operation-end');

// 分析 React 组件渲染
import { ReactDevToolsProfiler } from 'react';

// 或使用 useMemo/useCallback 减少重渲染
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

详细解析

Performance 面板使用步骤:

  1. 录制前: 勾选 "Screenshots" 和 "Web Vitals",选择适当网络节流
  2. 录制中: 执行待分析的操作(滚动、点击、数据加载等)
  3. 录制后: 分析火焰图,找出耗时任务

常见性能问题模式:

火焰图特征 问题 解决方案
大量短任务堆积 频繁 setState useDeferredValue
单个长任务 复杂计算 Web Worker / 任务拆分
Layout thrashing 读写交替触发重排 批量读写
长时间 GC 内存分配过多 对象池/避免频繁创建

Web Vitals 自动化监控:

javascript 复制代码
import { getCLS, getFID, getLCP } from 'web-vitals';

function sendToAnalytics({ name, value, id }) {
  // 上报至数据分析服务
  navigator.sendBeacon('/analytics', JSON.stringify({ name, value, id }));
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
相关推荐
进击切图仔1 小时前
RAG 加载 pdf 文档
linux·前端·pdf
小小小小宇1 小时前
git 大仓库拉取卡顿问题
前端
用户600071819101 小时前
【翻译】在 React Router 中理清对话框
react.js
前端那点事1 小时前
告别低级冗余!10个前端原生高阶技巧,让代码更优雅、性能更出众
前端·vue.js
hexu_blog1 小时前
前端vue后端java如何实现证件照功能
前端·javascript·vue.js
豹哥学前端1 小时前
前端 LocalStorage 实战:从入门到熟练,一篇就够了
前端·javascript·面试
用户40189933422841 小时前
第 11 章 MCP 协议与集成
前端
Southern Wind1 小时前
谷记账——一个 Vue 3 批次记账 App
前端·javascript·vue.js
lzhdim2 小时前
SQL 入门 14:SQL 触发器与事件:自动化数据处理
linux·前端·数据库·sql·自动化