普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月23日首页

React性能优化:从“卡成狗”到“丝般顺滑”的5个秘诀

作者 kyriewen
2026年4月23日 12:04

前言

React已经很快了,但如果你不注意细节,它会做很多“无用功”:组件没必要的重渲染、大列表全量渲染、状态更新导致整个页面刷新……这些问题累积起来,再好的电脑也扛不住。

今天我们不聊虚拟DOM原理,直接上代码、上工具,告诉你哪些写法是“性能杀手”,哪些是“救星”。优化完,你的React应用会快得让用户怀疑是不是装了外挂。

一、第1招:用React.memo避免“父动子也动”

React默认:父组件更新,所有子组件都会重新渲染(即使props没变)。这会导致大量浪费。

差代码

const Child = ({ name }) => {
  console.log('Child渲染了');
  return <div>{name}</div>;
};

const Parent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(c => c+1)}>点击 {count}</button>
      <Child name="张三" />
    </div>
  );
};

每次点击按钮,Child都会重新渲染,但它的name根本没变。

好代码:用React.memo包装子组件。

const Child = React.memo(({ name }) => {
  console.log('Child渲染了');
  return <div>{name}</div>;
});

现在只有name变化时,Child才会重新渲染。

注意:如果Child接收的props包含函数或对象,需要配合useCallbackuseMemo(见第2招)。

二、第2招:用useCallbackuseMemo缓存函数和值

在React组件里,每次渲染都会重新创建所有内联函数和对象。即使内容相同,引用也不同,导致React.memo失效。

差代码

const Parent = () => {
  const handleClick = () => console.log('clicked'); // 每次渲染都是新函数
  return <Child onClick={handleClick} />;
};

好代码:用useCallback缓存函数。

const Parent = () => {
  const handleClick = useCallback(() => console.log('clicked'), []); // 依赖为空,永远不变
  return <Child onClick={handleClick} />;
};

对于复杂计算的值(比如过滤大列表),用useMemo缓存结果:

const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

三、第3招:虚拟列表,渲染一万条也不怕

直接渲染长列表(比如聊天记录、商品列表)会导致浏览器创建上万个DOM节点,内存爆炸,滚动卡顿。虚拟列表只渲染可视区域内的几条,滚动时动态替换。

不用自己造轮子,用现成库

  • react-window(轻量)
  • react-virtualized(功能全)
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>行 {index}</div>
);

<List
  height={400}
  itemCount={10000}
  itemSize={35}
  width={300}
>
  {Row}
</List>

一秒渲染一万条,滚动丝滑。

四、第4招:代码分割 + 懒加载,别一次加载所有组件

你的用户访问首页,结果你把后台管理、用户设置、订单详情所有页面的代码都下载了。浪费流量,也拖慢首屏。

React.lazy + Suspense

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

每个路由的代码单独打包,只有访问时才加载。

五、第5招:避免内联对象和函数传递

即使不用memo,内联对象也会导致子组件每次接收新对象,触发重渲染。

差代码

<Child style={{ color: 'red' }} />  // 每次渲染都是新对象

好代码:把对象提取到组件外部或使用useMemo

const childStyle = { color: 'red' }; // 外部定义,引用不变
<Child style={childStyle} />

六、额外绝招:使用useTransition标记非紧急更新

React 18引入了useTransition,可以把某些更新标记为“低优先级”,让高优先级交互(如输入框打字)更流畅。

const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [list, setList] = useState([]);

const handleChange = (e) => {
  const value = e.target.value;
  setQuery(value); // 紧急更新:更新输入框
  startTransition(() => {
    // 非紧急更新:过滤大列表
    const filtered = hugeList.filter(item => item.includes(value));
    setList(filtered);
  });
};

这样打字不会卡,列表过滤稍后完成。

七、工具检测:React DevTools Profiler

安装React DevTools,打开Profiler标签,录制一段操作,可以看到每个组件渲染耗时。颜色越黄/红,越需要优化。

八、总结:优化口诀

  • 子组件纯展示,包上React.memo
  • 函数和对象,用useCallbackuseMemo缓存。
  • 长列表用虚拟滚动。
  • 路由懒加载,按需取。
  • 内联对象移出去,引用不变。
  • 紧急更新与非紧急分开,用useTransition

优化完,你的React应用会快得飞起。用户会惊叹:“这网站怎么比原生App还流畅?”

昨天 — 2026年4月22日首页

页面滚动卡成PPT?这5招让你的动画丝滑如德芙

作者 kyriewen
2026年4月21日 20:39

你刚写了个酷炫的滚动动画,结果一滑,帧率掉到10帧,电脑风扇狂转,用户直接关掉网页。今天我们不聊首屏,专治“运行时卡顿”——滚动、动画、输入框打字都能卡成狗。5招下去,让你的页面像吃了德芙,纵享丝滑。

前言

你有没有这种体验:滑动页面,感觉像在拖拽一块湿水泥;鼠标滚轮滚一下,页面半秒后才动;输入框打字,字母一个一个蹦出来。这就是运行时性能差——不是加载慢,而是交互不流畅。

原因通常是:重排重绘太频繁、JS执行时间太长、动画没用GPU。今天我们就来逐一击破,让你写出60帧满跑的页面。

一、帧率是怎么回事?60帧才是丝滑

浏览器理想刷新率是60fps,也就是每16.6毫秒要渲染一帧。如果JS任务或渲染任务超过这个时间,就会丢帧,用户就感觉“卡”。

Chrome DevTools → Performance 录制,看帧率条,红色就是掉帧了。我们的目标:每帧任务控制在10ms以内,留出余量。

二、第1招:用transformopacity做动画,别动left/top

最经典的性能优化。修改lefttopwidthmargin会触发重排(Layout),修改颜色、背景会触发重绘(Paint),而修改transformopacity只触发合成(Composite),直接走GPU,完全不卡。

差代码

.box {
  transition: left 0.3s;
  left: 0;
}
.box.active {
  left: 100px;
}

好代码

.box {
  transition: transform 0.3s;
  transform: translateX(0);
}
.box.active {
  transform: translateX(100px);
}

记住:能用transform绝不用left,能用opacity绝不用visibility

三、第2招:滚动事件用passiverequestAnimationFrame

滚动时触发scroll事件,如果你在里面做复杂操作,浏览器会等你的代码执行完才滚动,导致卡顿。

解决方案1:passive: true
告诉浏览器:我不会调用preventDefault(),你可以直接滚动。

window.addEventListener('scroll', handler, { passive: true });

解决方案2:用requestAnimationFrame节流
滚动事件触发频率很高,不需要每一帧都处理。用requestAnimationFrame保证只在浏览器要渲染时才执行。

let ticking = false;
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      // 做滚动相关操作
      ticking = false;
    });
    ticking = true;
  }
});

四、第3招:输入框防抖,别每敲一个字都发请求

搜索框实时搜索,用户每打一个字母就发请求,不仅卡,还把服务器打爆。

防抖(debounce):用户停止输入300ms后才执行。

function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
const search = debounce((keyword) => {
  fetch('/search?q=' + keyword);
}, 300);
input.addEventListener('input', (e) => search(e.target.value));

五、第4招:虚拟列表,一万条数据也不怕

渲染长列表(比如聊天记录、商品列表)时,一次性生成所有DOM节点会导致页面卡死。虚拟列表只渲染可视区域内的几条,滚动时动态替换。

实现思路:监听滚动,计算当前显示哪些索引,只创建这些DOM。推荐直接用库:react-windowvue-virtual-scroller

import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => <div style={style}>行 {index}</div>;
<List height={400} itemCount={10000} itemSize={35}>
  {Row}
</List>

瞬间渲染一万条,丝滑。

六、第5招:Web Worker,把重活丢到后台

复杂计算(比如数据加密、图像处理、大量数据排序)会阻塞主线程,导致页面无法交互。用Web Worker在后台线程执行,完事通知主线程。

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeData);
worker.onmessage = (e) => {
  console.log('计算结果', e.data);
};

注意:Worker里不能操作DOM,只能做计算。

七、额外绝招:避免强制同步布局

当你在JS里读取布局属性(offsetTopclientWidth等),又紧接着修改样式,浏览器会强制同步重排,非常卡。

// 坏
boxes.forEach(box => {
  box.style.width = box.offsetWidth + 'px'; // 读,触发重排
});
// 好
const widths = boxes.map(box => box.offsetWidth); // 先全读
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 'px'; // 再全写
});

读写分离,批量操作。

八、工具检测:Performance面板使用技巧

  • 录制一段操作,看火焰图里长任务(Long Task)——超过50ms的任务会标记为红色。
  • 勾选“Screenshots”能看到卡顿时的画面。
  • 看“Summary”标签,如果“Layout”或“Paint”占比高,说明需要减少重排重绘。

九、总结:运行时优化口诀

  • 动画用transform,不用left/top
  • 滚动加passive,事件用rAF
  • 输入做防抖,长列表用虚拟。
  • 重计算丢Worker,读写要分离。

优化完,你再滑动页面,就像摸到丝绸一样顺滑。用户会惊讶:“这网站怎么这么快?”


如果你觉得今天的“丝滑课”够流畅,点个赞让更多人看到。明天我们聊聊前端工程化——从脚手架到自动化部署,让你一键发布,告别手动上传FTP。我们明天见!

昨天以前首页

你的首屏慢得像蜗牛?这6招让页面“秒开”

作者 kyriewen
2026年4月20日 17:51

用户打开你的网站,3秒了还是一片白。他走了,去了隔壁。你丢了一个客户,就因为首屏慢了几秒。今天我们来给页面“提速”,6个实战技巧,从网络请求到渲染,让你的首屏加载快得像闪电。

前言

你有没有等过一个加载超过5秒的网页?那种感觉就像在机场等一艘船。用户耐心有限:3秒内没打开,一半人会走。今天我们不谈虚的理论,直接上代码、上配置、上工具,从源头把首屏时间砍掉一半以上。

一、首屏慢的三大元凶

  • 请求太多:几十个JS、CSS、图片,每个都要握手、传输。
  • 资源太大:未压缩的图片、没Tree Shaking的依赖。
  • 渲染阻塞:CSS和JS阻塞了HTML解析,白屏时间拉长。

对症下药,我们一个个击破。

二、第1招:SSR或预渲染,让首屏“有内容”

纯SPA(单页应用)的HTML几乎是空的,需要等JS下载执行后才渲染。用户看到白屏的时间很长。

解决方案

  • SSR(服务端渲染):用Next.js(React)或Nuxt(Vue),在服务器生成完整HTML,用户直接看到内容,然后JS“水合”绑定事件。
  • 静态生成(SSG):像Gatsby、Astro,构建时生成HTML,适合内容不频繁变化的页面。
  • 预渲染(Prerendering):用prerender-spa-plugin在构建时把几个关键路由生成静态HTML。

如果你不想上SSR,至少做到骨架屏——在JS执行前先显示灰色占位块,让用户觉得“快了快了”。

三、第2招:代码分割,别一次加载所有

你只访问首页,结果整个后台管理系统的代码都下载了。浪费流量,也浪费时间。

Webpack/Vite内置代码分割

  • 动态导入(import()):路由级别的懒加载。
// 路由懒加载
const UserPage = () => import('./pages/UserPage');
  • 分割第三方库:把reactlodash等抽成单独的vendor文件,利用缓存。
// vite.config.js
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ['react', 'react-dom'],
        ui: ['antd']
      }
    }
  }
}

四、第3招:压缩与优化资源

图片:首屏最大杀手

  • 换成WebP:比JPEG小30%左右。用<picture>标签提供fallback。
  • 懒加载:首屏之外的图片先不加载,滚动到再加载。
<img loading="lazy" src="..." alt="...">
  • 响应式图片:用srcset给不同屏幕尺寸加载不同大小的图片。

字体:FOIT(无样式文本闪烁)

  • font-display: swap先显示系统字体,等自定义字体加载完再替换。
  • 只加载需要的字符集(比如只加载英文和数字)。

JS/CSS压缩

  • Vite/Webpack生产模式默认开启压缩。但可以手动配置Terser去掉console
  • compression-webpack-plugin生成gzip或brotli文件,让服务器直接返回压缩版本。

五、第4招:优化关键渲染路径

浏览器先解析HTML,遇到<link><script>会阻塞渲染。

内联关键CSS

把首屏需要的CSS直接内联到<style>里,其余CSS异步加载。

<style>/* 首屏CSS */</style>
<link rel="preload" href="main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

给JS加defer或async

  • defer:并行下载,但按顺序执行,在DOMContentLoaded之前执行。
  • async:并行下载,下载完立刻执行,执行顺序不定。
<script defer src="app.js"></script>

对于首屏不需要的JS,可以延迟到页面空闲时加载。

// 空闲时加载
requestIdleCallback(() => import('./analytics.js'));

六、第5招:使用CDN和HTTP/2

  • CDN:把静态资源放到离用户最近的服务器,减少物理距离导致的延迟。
  • HTTP/2:多路复用,一个连接并发传输多个文件,比HTTP/1.1的6个连接限制强很多。

七、第6招:缓存策略,二次访问秒开

  • 强缓存Cache-Control: max-age=31536000(一年),适用于不变的资源(带hash的JS/CSS)。
  • 协商缓存ETag + Last-Modified,服务器确认资源没变化则返回304。
  • Service Worker:离线缓存,甚至可以做到“骨架屏秒现”。

八、实战:用Lighthouse跑分并优化

Chrome DevTools → Lighthouse,生成报告,它会告诉你哪些资源浪费了时间、哪些图片可以优化、哪些请求阻塞渲染。

常见优化建议:

  • 移除阻塞渲染的脚本。
  • 压缩图片。
  • 减少未使用的CSS(用purgecss移除没用的样式)。
  • 启用文本压缩(gzip)。

九、总结:首屏优化清单

  • 开启Gzip/Brotli压缩。
  • 图片转WebP、懒加载、响应式。
  • 路由懒加载 + 第三方库分割。
  • 关键CSS内联,非关键异步加载。
  • JS加defer/async。
  • 使用CDN + HTTP/2。
  • 配置强缓存和协商缓存。
  • 用Lighthouse反复测量。

优化完,你的页面首屏时间可以从3秒降到1秒以内。用户开心,老板也开心。


如果你觉得今天的提速课够实战,点个赞让更多人看到。明天我们继续性能优化第二弹——运行时优化,让你的页面滚动、动画、输入都不掉帧。我们明天见!

代码写成一锅粥?这5种设计模式让你的项目“起死回生”

作者 kyriewen
2026年4月18日 22:51

你是不是见过这样的代码:一个文件几千行,一个函数做了十件事,改一个地方崩三个地方。今天我们不背理论,直接用5种前端最常用的设计模式,把你从“面条代码”里捞出来。学完你会发现:原来代码可以像乐高一样,哪里坏了换哪里。

前言

设计模式不是“高大上”的面试题,而是前辈们踩过无数坑后总结的“套路”。就像下棋有定式,写代码也有常见问题的标准解法。今天我们从实际场景出发,不讲23种全部,只挑前端最常用的5种:单例、观察者、工厂、策略、装饰器。看完你就能立刻用在项目里。

一、单例模式:全局只有一个的“独生子”

场景:全局弹窗、登录框、Store、线程池。你希望整个应用只有一个实例,反复创建会浪费资源或导致状态冲突。

不用的痛:每次调用都new Modal(),结果页面上出现十几个重叠的弹窗。

实现

class Singleton {
  constructor() {
    if (!Singleton.instance) {
      this.data = [];
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
  add(item) {
    this.data.push(item);
  }
}
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true

前端更常见的写法:用闭包或模块(ES6模块本身就是单例)。

// modal.js
let instance;
export function getModal() {
  if (!instance) {
    instance = new Modal();
  }
  return instance;
}

现代替代:直接导出对象字面量(export default { show() {} }),ES6模块天然单例。

二、观察者模式:让不相干的组件“悄悄对话”

场景:购物车更新后,导航栏的数字要变、价格要重算、埋点要上报。你不想让购物车直接调用导航栏的方法(耦合太紧)。

不用的痛:购物车里写header.updateCartCount()sidebar.recalculate()analytics.track()…每加一个模块,购物车代码就要改一次。

实现

class EventBus {
  constructor() {
    this.events = {};
  }
  on(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
  }
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(cb => cb(data));
    }
  }
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}
const bus = new EventBus();
// 购物车模块
bus.emit('cartUpdated', { count: 3 });
// 导航栏模块
bus.on('cartUpdated', (data) => updateCount(data.count));
// 埋点模块
bus.on('cartUpdated', (data) => track('cart', data));

观察者模式是前端最常用的模式之一。Vue的响应式原理、React的事件系统、Node.js的EventEmitter都是它的变体。

三、工厂模式:不用自己 new,让“工厂”替你造

场景:根据不同参数创建不同类型的对象,但创建逻辑复杂(比如需要条件判断、依赖注入)。你不想在业务代码里到处写newif-else

不用的痛:每个用到按钮的地方都要写一堆if (type === 'primary') return new PrimaryButton()…重复代码爆炸。

实现

class ButtonFactory {
  createButton(type) {
    switch(type) {
      case 'primary':
        return new PrimaryButton();
      case 'danger':
        return new DangerButton();
      default:
        return new DefaultButton();
    }
  }
}
const factory = new ButtonFactory();
const btn = factory.createButton('primary');

工厂模式把创建对象的逻辑集中管理,业务代码只依赖工厂接口。

更简单的函数工厂

function createUser(role) {
  const base = { createdAt: Date.now() };
  if (role === 'admin') {
    return { ...base, permissions: ['read', 'write', 'delete'] };
  }
  return { ...base, permissions: ['read'] };
}

四、策略模式:消灭“if-else 毒瘤”

场景:表单校验:用户名规则、密码规则、邮箱规则各不相同。或者根据用户等级计算折扣:普通会员9折,黄金会员8折,钻石会员7折。你不想写一长串if-else

不用的痛:一个函数里十几个if-else,加一个新策略要改原有代码,还容易引入bug。

实现

// 策略对象
const discountStrategies = {
  normal: (price) => price * 0.9,
  gold: (price) => price * 0.8,
  diamond: (price) => price * 0.7,
};
function getDiscount(level, price) {
  return discountStrategies[level]?.(price) ?? price;
}
// 使用时
const finalPrice = getDiscount('gold', 100); // 80

校验示例:

const validators = {
  required: (val) => val.trim() !== '',
  minLength: (val, len) => val.length >= len,
  email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
};
function validate(value, rules) {
  for (let rule of rules) {
    const [name, param] = rule.split(':');
    if (!validators[name]?.(value, param)) return false;
  }
  return true;
}

策略模式把算法(策略)提取成独立对象,可以动态替换、复用。

五、装饰器模式:给代码“贴金”而不改源码

场景:给现有函数添加日志、性能监控、权限校验、缓存功能,但不修改函数本身。

不用的痛:在每个函数内部手动加console.time,加完又删,污染业务逻辑。

实现(JavaScript高阶函数版本,TS装饰器已在之前文章讲过):

function withLog(fn) {
  return function(...args) {
    console.log(`调用 ${fn.name} 参数:`, args);
    const result = fn.apply(this, args);
    console.log(`返回值:`, result);
    return result;
  };
}
function add(a, b) { return a + b; }
const loggedAdd = withLog(add);
loggedAdd(2, 3); // 输出日志,返回5

更通用的装饰器组合:

function withTimer(fn) {
  return function(...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    const end = performance.now();
    console.log(`${fn.name} 耗时 ${end - start}ms`);
    return result;
  };
}
// 组合多个装饰器
const enhanced = withLog(withTimer(add));

装饰器模式让你能“叠加”功能,保持单一职责。

六、实际项目中的组合运用

比如一个用户登录模块:

  • 单例模式:全局唯一的UserStore
  • 工厂模式:根据角色创建不同的用户实例(createUser('admin'))。
  • 观察者模式:登录成功后,触发userLoggedIn事件,购物车、头像组件、权限菜单分别响应。
  • 策略模式:不同等级用户的权限校验策略。
  • 装饰器模式:给API请求函数加上缓存、重试、日志。

七、总结:设计模式是“招式”,不是“教条”

  • 单例:全局唯一,省资源。
  • 观察者:解耦事件发布和订阅。
  • 工厂:集中创建对象。
  • 策略:消灭if-else,算法可互换。
  • 装饰器:动态增强功能。

不要为了用模式而用模式。当你的代码出现重复、难维护、改一处动全身时,想想哪种模式能帮你解耦。写代码就像搭积木,模式是那些标准接口的积木块,让你搭得又快又稳。


如果你觉得今天的“招式”够实用,点个赞让更多人看到。明天我们将聊聊前端架构设计——从技术选型到目录结构,如何搭建一个能支撑三年迭代的项目骨架。我们明天见!

❌
❌