阅读视图

发现新文章,点击刷新页面。

Vue 3 性能优化的 5 个隐藏技巧,第 4 个连老手都未必知道

上周,我们上线了一个数据看板页面,本地跑得飞快,一上生产——滚动卡成 PPT

Profiler 一抓,发现:

  • 每次滚动都在重复创建 computed 函数
  • 列表项里嵌套了 3 层 <Suspense>
  • 一个 watch 竟然监听了整个 reactive 对象……

问题不在逻辑,而在 “你以为没问题的写法”

今天,我就分享 5 个 Vue 3 中少有人提、但效果惊人的性能优化技巧,尤其第 4 个,连很多 5 年经验的老手都没用过。


技巧 1:别在模板里写“方法调用”,用 computed + 缓存

反面教材:

<template>
  <div>{{ formatUserName(user) }}</div> <!-- 每次渲染都执行! -->
</template>

<script setup>
const formatUserName = (user) => `${user.firstName} ${user.lastName}`;
</script>

正确做法:

const formattedName = computed(() => 
  `${user.value.firstName} ${user.value.lastName}`
);
<template>
  <div>{{ formattedName }}</div> <!-- 响应式缓存,依赖不变不重算 -->
</template>

关键点:模板中的函数调用 没有缓存,每次 re-render 都会执行!


技巧 2:v-for 里的组件,记得加 key —— 但别用 index

很多人知道要加 key,但随手写:

<div v-for="(item, index) in list" :key="index">
  <ItemCard :data="item" />
</div>

问题:当列表发生插入/删除时,index 会变,导致 Vue 错误复用组件实例,引发状态错乱 or 不必要的销毁重建。

正确做法:用唯一 ID

<div v-for="item in list" :key="item.id">
  <ItemCard :data="item" />
</div>

如果真没 ID?考虑用 Symbol()crypto.randomUUID() 生成稳定 key(仅限静态列表)。


技巧 3:慎用 watch 监听整个 reactive 对象

const state = reactive({ a: 1, b: 2, c: 3 });

watch(state, () => {
  console.log('state changed');
});

这会导致:只要 abc 任意一个变了,回调就触发,即使你只关心 a

更精准的写法:

// 方案 A:监听具体属性
watch(() => state.a, (newVal) => { ... });

// 方案 B:用 toRefs 解构后监听
const { a } = toRefs(state);
watch(a, (newVal) => { ... });

高级技巧:如果必须监听多个字段,用 getter 函数组合:

watch(
  () => ({ a: state.a, b: state.b }),
  (newVals) => { /* 只有 a 或 b 变才触发 */ }
);

技巧 4:用 shallowRefmarkRaw 跳过不必要的响应式(隐藏大招!)

这是 Vue 3 响应式系统中最被低估的 API

场景:你有一个大型配置对象 or 第三方库实例(如 echarts 实例),不需要响应式?

默认写法(性能杀手):

const chart = ref(null); // Vue 会尝试把 echarts 实例变成响应式!
onMounted(() => {
  chart.value = echarts.init(dom); // 内部 thousands of properties!
});

正确做法:

// 方案 A:用 shallowRef(只让 .value 响应,内部不递归)
const chart = shallowRef(null);

// 方案 B:用 markRaw 明确告诉 Vue “别动它”
const chartInstance = markRaw(echarts.init(dom));
const chart = ref(chartInstance);

效果:避免 Vue 递归遍历大型对象,节省内存 + 提升初始化速度 10x+

适用场景:

  • 图表实例(ECharts、Chart.js)
  • 复杂配置对象(如 Monaco Editor options)
  • 不变的数据结构(如路由 meta、常量字典)

技巧 5:懒加载组件 + 异步 setup,减少首屏负担

别让所有组件都在首屏加载!

<!-- 同步引入,打包进主 chunk -->
<script setup>
import HeavyChart from './HeavyChart.vue';
</script>

改成动态导入 + Suspense:

<template>
  <Suspense>
    <template #default>
      <LazyChart />
    </template>
    <template #fallback>
      <div>Loading chart...</div>
    </template>
  </Suspense>
</template>

<script setup>
// 自动代码分割
const LazyChart = defineAsyncComponent(() => import('./HeavyChart.vue'));
</script>

进阶:配合 IntersectionObserver 实现滚动到可视区再加载

const isVisible = ref(false);
// 当元素进入视口,isVisible = true → 再加载组件

总结:5 个技巧速查表

技巧 适用场景 性能收益
模板中用 computed 代替方法调用 频繁渲染的格式化逻辑 避免重复计算
v-for 用唯一 ID 做 key 动态列表(增删改) 减少 DOM 重建
精准 watch 而非监听整个对象 复杂状态管理 避免无效回调
shallowRef / markRaw 跳过响应式 大型对象、第三方实例 内存 & 初始化提速
异步组件 + Suspense 重型组件(图表、编辑器) 首屏加载更快

最后说两句

Vue 3 的性能,80% 取决于你如何使用响应式系统,而不是框架本身慢。

真正的优化,不是“加缓存”“开 SSR”,而是:

在正确的地方,用正确的 API,做最小化的响应式。

下次写组件前,先问自己:

“这个数据,真的需要响应式吗?”


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

别再无脑用 `JSON.parse()` 了!这个安全漏洞你可能每天都在触发

你以为只是解析个字符串?其实黑客已经在你服务器上跑脚本了!

在前端和 Node.js 开发中,JSON.parse() 几乎无处不在:

const data = JSON.parse(localStorage.getItem('user'));
const config = JSON.parse(req.body.payload);
const settings = JSON.parse(fs.readFileSync('config.json'));

简洁、直接、好用——但极其危险

如果你没有对输入做任何校验就调用 JSON.parse(),你正在为应用打开一扇“任意代码执行”的后门。

今天,我们就来揭开 JSON.parse() 背后的安全雷区,并告诉你如何用更安全、更现代的方式处理 JSON 数据。


危险场景一:原型污染(Prototype Pollution)

这是 JSON.parse() 最臭名昭著的安全漏洞之一。

虽然原生 JSON.parse() 本身不会执行代码,但它会忠实地还原对象结构——包括 __proto__constructor.prototype 这类特殊属性。

来看一个真实攻击载荷:

const userInput = '{"__proto__":{"isAdmin":true}}';
const obj = {};
JSON.parse(userInput, (key, value) => {
  obj[key] = value;
  return value;
});
console.log({}.isAdmin); // true!全局对象被污染!

如果这段代码出现在你的登录逻辑、权限校验或配置合并中,攻击者就能:

  • 绕过身份验证(isAdmin: true);
  • 注入恶意属性(如 exec: 'rm -rf /');
  • 篡改全局行为,导致服务崩溃或数据泄露。

尤其在使用 Lodash、merge、assign 等工具库时,风险更高!


危险场景二:拒绝服务(DoS)

恶意构造的 JSON 字符串可导致内存爆炸CPU 耗尽

// 深度嵌套攻击
const evil = '{"a":{"a":{"a":{"a":{"a":{"a": ... }}}}}}';

// 或超大数组
const evil2 = '[1,1,1,...,1]' // 1000 万个元素

调用 JSON.parse(evil) 可能:

  • 占用数 GB 内存;
  • 阻塞事件循环数秒;
  • 直接触发 OOM(Out of Memory)崩溃。

在 API 接口或 Webhook 处理中,这等于把“关机按钮”交给了攻击者。


正确姿势:安全解析 JSON 的三重防护

第一步:限制输入大小

在解析前先检查字符串长度:

function safeParse(str, maxSize = 1024 * 100) { // 100KB
  if (typeof str !== 'string' || str.length > maxSize) {
    throw new Error('Input too large');
  }
  return JSON.parse(str);
}

第二步:禁用危险键(如 __proto__

使用 reviver 函数过滤敏感属性:

function secureJSONParse(str) {
  return JSON.parse(str, (key, value) => {
    if (key === '__proto__' || key === 'constructor') {
      throw new Error('Disallowed key in JSON');
    }
    return value;
  });
}

第三步(推荐):用 Zod / Joi 做运行时校验

这才是现代 JS 工程的最佳实践!

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  isAdmin: z.boolean().optional(),
});

function parseUser(jsonStr: string) {
  const raw = secureJSONParse(jsonStr);
  return UserSchema.parse(raw); // 自动校验 + 类型推导
}

优势:

  • 类型安全(配合 TypeScript 完美);
  • 自动过滤多余字段
  • 明确拒绝非法结构
  • 防止原型污染、字段注入等攻击

特别提醒:Node.js 中的额外风险

在服务端,如果你从以下来源解析 JSON,风险更高:

  • HTTP 请求体(req.body
  • 文件读取(用户上传的 JSON 配置)
  • Redis / 数据库存储的序列化数据
  • 第三方 Webhook 回调

务必在解析前做来源校验 + 结构校验 + 大小限制三重保险!


结语

JSON.parse() 不是“坏 API”,但它是一把没有保险的枪
在现代 Web 开发中,信任任何用户输入 = 自毁程序

下次当你写下 JSON.parse(someString) 时,请自问:

“我确定这个字符串来自可信源吗?它的结构真的安全吗?”

如果答案不确定,请立即切换到 Zod / Joi + 安全解析函数 的组合。

转发给那个还在裸用 JSON.parse() 的队友吧!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

别再让 `console.log` 上线了!它正在悄悄拖垮你的生产系统

你以为只是“打个日志”?其实它在泄露数据、吃光内存、暴露源码!

在开发过程中,console.log() 是我们最亲密的伙伴:

function calculatePrice(items) {
  console.log('items:', items); // 调试用
  return items.reduce((sum, item) => sum + item.price, 0);
}

方便、直观、零成本——但一旦这段代码被部署到生产环境,隐患就开始蔓延

今天我们就来揭开 console.log 在生产环境中的三大“罪状”,并告诉你如何彻底杜绝它。


危害一:敏感信息泄露

这是最致命的问题。

你在本地调试时可能这样写:

console.log('User login:', { email, password });
console.log('DB connection string:', process.env.DB_URL);
console.log('Admin token:', req.headers.authorization);

如果这些日志随代码上线:

  • 用户密码、API 密钥、数据库地址 会直接打印到服务器控制台;
  • 如果你用了 PM2、Docker、K8s 或云平台(如阿里云、AWS),这些日志会被自动采集到日志系统;
  • 任何有日志权限的运维、实习生、外包人员都能看到!
  • 更糟的是,如果日志被错误地公开(比如 GitHub 泄露、ELK 未设权限),黑客将直接拿到“系统钥匙”。

真实案例:2023 年某电商因 console.log 泄露支付密钥,导致数万元盗刷。


危害二:性能损耗与内存泄漏

别小看一个 console.log,它在高并发下是“隐形杀手”。

1. 同步 I/O 阻塞

Node.js 中的 console.log 默认是同步写入 stdout 的(尤其在非 TTY 环境,如 Docker 容器)。
这意味着:每打一行日志,事件循环都会被短暂阻塞。

在 QPS 1000+ 的接口中,频繁 console.log 可能导致:

  • 响应延迟增加 10%~30%;
  • CPU 使用率异常飙升;
  • 请求排队甚至超时。

2. 大对象序列化开销

console.log('Full user object:', hugeUserData); // 包含头像 Buffer、历史订单等

console.log 会调用 .toString() 或内部序列化逻辑,若对象巨大(如图片 Buffer、长数组),会:

  • 消耗大量 CPU;
  • 生成超长字符串,占用堆内存;
  • 触发频繁 GC,甚至 OOM 崩溃。

危害三:暴露源码结构与业务逻辑

生产环境的日志往往会被集中管理(如 Sentry、Datadog、阿里云 SLS)。
如果你不小心把函数名、变量名、内部路径打出来:

console.log('Calling internal service: /v1/billing/calculate-discount');
console.log('Error in function: validatePromoCodeV2');

攻击者就能:

  • 推测你的 API 设计;
  • 发现未公开的内部接口;
  • 结合其他漏洞发起精准攻击(如 IDOR、越权)。

这等于主动给黑客画地图


正确姿势:用专业日志系统替代 console.log

第一步:开发阶段就禁用生产级日志输出

使用环境判断(但不推荐仅靠这个!):

if (process.env.NODE_ENV !== 'production') {
  console.log('Debug info:', data);
}

问题:容易遗漏,且无法防止“忘记删除”的日志。


第二步(强烈推荐):引入专业日志库

使用 WinstonBunyanPino 等结构化日志工具:

import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
  transports: [
    new winston.transports.Console(),
    // 生产环境可加文件、Sentry、阿里云 SLS 等
  ],
});

// 安全地记录
logger.debug('User data', { userId: user.id }); // 不会打印完整对象
logger.error('Payment failed', { orderId, reason });

优势:

  • 支持日志级别(debug/info/warn/error);
  • 自动过滤敏感字段(可通过 format 实现);
  • 异步/高性能输出;
  • 与监控系统无缝集成。

第三步:构建时自动清除 console.log

在打包阶段用工具彻底移除:

Webpack:

// webpack.config.js
optimization: {
  minimizer: [
    new TerserPlugin({
      terserOptions: {
        compress: {
          drop_console: true, // 删除所有 console.*
        },
      },
    }),
  ],
}

Vite / Rollup:

使用插件如 rollup-plugin-stripvite-plugin-remove-console

ESLint(预防):

配置规则禁止提交 console

{
  "rules": {
    "no-console": "warn"
  }
}

配合 Git Hooks(如 husky + lint-staged),提交前自动检查。


终极建议:建立“日志规范”

  • 绝不在生产代码中使用 console.log
  • 所有日志必须通过统一 logger 实例输出
  • 敏感字段(密码、token、身份证)必须脱敏
  • 日志内容需经过安全审计

结语

console.log 是开发的好帮手,但它是生产环境的毒药
一次疏忽,可能导致数据泄露、服务崩溃、甚至法律风险。

记住:

真正的专业,不是能写出功能,而是能守住底线。

从今天起,让 console.log 止步于你的本地开发机。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

别再用 scoped 了!Vue 项目中真正安全的 CSS 封装方案,第 3 种连尤雨溪都在用

上周,设计师跑来问我:“为什么这个按钮在 A 页面是蓝色,在 B 页面变成紫色了?”

我一查代码,发现两个组件都写了:

.btn {
  background: blue;
}

<style scoped> 根本没生效——因为某个第三方 UI 库用了 :global(.btn),污染了全局。

那一刻我悟了:scoped 不是银弹,它只是“看起来安全”。

今天,我就带你盘点 Vue 项目中 4 种真正可靠的 CSS 封装方案,从“能用”到“企业级”,尤其第 3 种,连 Vue 官方文档和 Vite 团队都在悄悄推广。


先看一张对比表(建议收藏)

方案 隔离性 可维护性 支持动态主题 学习成本
<style scoped> ⚠️ 中(会被 :global 破坏) 低(命名仍可能冲突) ❌ 难
CSS Modules ✅ 强 ⚠️ 需额外处理
CSS-in-JS(如 Vanilla Extract) ✅✅ 极强 ✅ 原生支持 中高
CSS 变量 + 作用域类名(推荐!) ✅ 强 ✅✅ 极高 ✅✅ 天然支持

核心原则:隔离靠机制,不是靠“看起来不一样”


方案 1:<style scoped> —— 谨慎使用!

Vue 的 scoped 通过给元素加 data-v-xxxx 属性实现样式隔离:

<template>
  <button class="btn">Click</button>
</template>

<style scoped>
.btn { color: red; } /* 编译后 → .btn[data-v-f3f3eg9] */
</style>

致命缺陷

  • 无法防止 全局样式污染(比如 reset.css 或 UI 库)
  • 深度选择器>>>:deep())容易误伤其他组件
  • 动态插入的 HTML(如富文本)无法应用 scoped 样式

适用场景:内部工具、小型页面、快速原型

不要用在:对外组件库、多团队协作项目、需要主题切换的系统


方案 2:CSS Modules —— 经典但略重

启用后,每个 class 会被哈希化:

// Button.module.css
.primary { background: blue; }

// Button.vue
import styles from './Button.module.css';
// styles.primary → "Button_primary__aB3cD"
<template>
  <button :class="styles.primary">OK</button>
</template>

优点:

  • 100% 隔离,不怕任何全局污染
  • 支持组合(composes

缺点:

  • 模板里写 :class="styles.xxx" 略啰嗦
  • 不支持原生 CSS 嵌套(除非配合 PostCSS)
  • 动态主题需配合 JS 重新生成

在 Vite 中开启:

// vite.config.ts
export default defineConfig({
  css: { modules: { localsConvention: 'camelCase' } }
})

方案 3:CSS 变量 + 作用域类名(尤雨溪团队推荐!)

这是 Vue 官方新文档Vite 插件生态 中越来越主流的做法。

核心思想:用 CSS 变量定义设计 token,用唯一类名包裹组件

<template>
  <div class="my-button--root">
    <button class="my-button--inner">Submit</button>
  </div>
</template>

<style>
.my-button--root {
  /* 定义局部变量 */
  --btn-bg: var(--theme-primary, #3b82f6);
  --btn-color: white;
}

.my-button--inner {
  background: var(--btn-bg);
  color: var(--btn-color);
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
}
</style>

神奇在哪?

  1. 天然支持主题切换
/* 全局定义亮色主题 */
:root {
  --theme-primary: #3b82f6;
}
/* 暗色主题 */
.dark {
  --theme-primary: #60a5fa;
}

只需切换 <html class="dark">,所有组件自动适配!

  1. 无构建时哈希,调试友好
  2. 类名前缀化(如 my-button--)避免冲突,比随机 hash 更语义化

这正是 ShadCN VueRadix Vue 等现代组件库的做法。


方案 4:零运行时 CSS-in-JS(Vanilla Extract)

如果你追求极致工程化,试试 编译时 CSS-in-JS

// Button.css.ts
import { style } from '@vanilla-extract/css';

export const root = style({
  vars: {
    '--btn-bg': '#3b82f6'
  }
});

export const inner = style({
  background: 'var(--btn-bg)',
  color: 'white',
  borderRadius: 4,
  selectors: {
    '&:hover': { opacity: 0.9 }
  }
});
<script setup lang="ts">
import * as styles from './Button.css';
</script>

<template>
  <div :class="styles.root">
    <button :class="styles.inner">OK</button>
  </div>
</template>

优势:

  • 100% 类型安全(TS 直接提示拼写错误)
  • 零运行时(编译成静态 CSS 文件)
  • 自动作用域(生成哈希类名)
  • 支持主题变量、条件样式

配合 Vite 插件 @vanilla-extract/vite-plugin 即可使用。


实战建议:怎么选?

项目类型 推荐方案
内部后台系统 CSS 变量 + 作用域类名(方案 3)
对外组件库 CSS 变量 + 作用域类名 or Vanilla Extract
快速原型 scoped(但警惕全局污染)
超大型应用(含多主题/国际化) Vanilla Extract(方案 4)

永远不要:

  • 在 scoped 中大量使用 :deep()
  • 把业务样式写进全局 app.css
  • 用 BEM 命名试图“人工隔离”(治标不治本)

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

别再乱用 ref 和 reactive 了!Vue 3 响应式最佳实践,90% 的人都踩过坑

上周 Code Review,我看到同事写了这样一段代码:

const state = reactive({
  user: null,
  loading: false,
  error: '',
  list: []
});

// 后面又单独定义
const currentPage = ref(1);
const pageSize = ref(10);

乍看没问题,但一运行**——页面卡顿、watch 失效、调试器里数据对不上……**

问题出在哪?
不是逻辑错,而是响应式对象的“组合方式”错了

今天,我就用3 条黄金法则 + 2 个实战模板,帮你彻底搞懂 Vue 3 响应式怎么写才高效、安全、可维护。

法则 1:简单值用 ref,复杂对象用 reactive —— 但别混用!

很多教程说:“primitive 用 ref,object 用 reactive”,这没错,但忽略了“解构陷阱”。

错误示范:

const { user, loading } = reactive({ user: null, loading: false });
// 解构后失去响应性!

正确做法:

// 方案 A:全部用 ref(推荐新手)
const user = ref(null);
const loading = ref(false);

// 方案 B:用 toRefs 保持响应性
const state = reactive({ user: null, loading: false });
const { user, loading } = toRefs(state); // ✅ 响应式保留

经验公式:

  • 如果你要频繁解构 or 传递单个属性 → 优先用 ref
  • 如果是完整状态模块(如表单、列表配置)→ 用 reactive + toRefs

法则 2:别把 ref 套进 reactive,除非你真的需要

见过这种写法吗?

const state = reactive({
  count: ref(0), // ❌ 不要!
  name: 'Vue'
});

这会导致:

  • 访问时必须写 state.count.value(破坏一致性)
  • 模板中虽然自动 unwrap,但逻辑层混乱
  • 容易引发“value 嵌套地狱”

正确做法:统一层级

// 要么全 ref
const count = ref(0);
const name = ref('Vue');

// 要么全 reactive(count 直接是 number)
const state = reactive({
  count: 0,
  name: 'Vue'
});

小技巧:在 setup() 返回时,用 ...toRefs(state) 一键暴露所有属性。

法则 3:大型组件,用“状态模块化”代替巨型 reactive

当组件状态超过 5 个字段,别堆在一个 reactive 里!

反面教材:

const state = reactive({
  // 用户信息
  userId, userName, userAvatar,
  // 分页
  page, size, total,
  // 搜索条件
  keyword, status, dateRange,
  // UI 状态
  showDrawer, loading, errorMsg...
});

推荐拆分:

// 按功能拆成多个小状态块
const userState = reactive({ id: '', name: '', avatar: '' });
const pagination = reactive({ page: 1, size: 10, total: 0 });
const uiState = reactive({ loading: false, drawerVisible: false });

// 或封装成 composable
const { userState } = useUserStore();
const { pagination, fetchList } = usePagination();

这样不仅逻辑清晰,还天然支持 逻辑复用(比如分页逻辑抽成 usePagination)。

实战模板:两种主流写法对比

模板 A:全 ref 风格(适合中小型组件)

export default {
  setup() {
    const loading = ref(false);
    const list = ref([]);
    const keyword = ref('');

    const search = async () => {
      loading.value = true;
      list.value = await api.search(keyword.value);
      loading.value = false;
    };

    return { loading, list, keyword, search };
  }
}

优点:直观、无解构风险、TS 类型推导友好
注意:返回时别漏写 .value

模板 B:reactive + toRefs(适合状态密集型组件)

export default {
  setup() {
    const state = reactive({
      loading: false,
      list: [] as Item[],
      keyword: ''
    });

    const search = async () => {
      state.loading = true;
      state.list = await api.search(state.keyword);
      state.loading = false;
    };

    return { ...toRefs(state), search };
  }
}

优点:状态聚合、减少变量声明、模板中直接用 list
注意:内部操作用 state.xxx,别解构!

高阶建议:结合 更清爽

如果你用 Vue 3.3+,直接上 :

import { ref } from 'vue'

const loading = ref(false)
const list = ref([])
const keyword = ref('')

const search = async () => {
  loading.value = true
  list.value = await api.search(keyword.value)
  loading.value = false
}

没有 return,没有 setup(),变量自动暴露——这才是 Vue 3 的终极舒适区。

最后说两句

Vue 3 的响应式系统很强大,但自由也意味着责任。
用对了,代码清爽如诗;用错了,bug 隐蔽如鬼。

记住三句话:

  1. 简单用 ref,复杂用 reactive
  2. 别混用,别嵌套,别解构裸对象
  3. 大组件,拆状态,抽 composable

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

❌