普通视图

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

你的首屏慢得像蜗牛?这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,算法可互换。
  • 装饰器:动态增强功能。

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


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

可选链 `?.`——再也不用写一长串 `&&` 了!

作者 kyriewen
2026年4月16日 18:35

引言

Cannot read property 'name' of undefined——又是这个错!我明明已经写了 if (user && user.profile && user.profile.name) 啊!”

同事凑过来看了一眼:“你只检查了 useruser.profile,但没检查 user.profile.name 本身?哦,其实 user.profile.name 不可能是 undefined 的……等等,如果 user.profile 是空对象呢?”

我陷入了沉思:难道我要写 user && user.profile && user.profile.name && user.profile.name.firstName?这代码长得像铁轨,谁看得懂啊!

直到有一天,我发现了可选链操作符 ?.。它就像一把瑞士军刀,轻轻一划,所有 undefined 的烦恼都烟消云散。

一、传统防守:&& 的“人肉护盾”

在过去,为了安全地访问深层嵌套的属性,我们不得不写这样的代码:

const firstName = user && user.profile && user.profile.name && user.profile.name.firstName;

如果中间任何一环是 nullundefined,整个表达式短路返回 undefined,不会报错。但这写法,读起来像在爬楼梯,每层都要确认一下。

更别提调用可能存在的方法:

const result = api && api.getData && api.getData();

万一 api.getData 不是函数?又得加判断。

二、可选链:.?. 的优雅空降

可选链操作符 ?. 允许你读取位于连接链深处的属性,而无需显式验证每一环是否有效。如果引用是 nullundefined,表达式短路返回 undefined

const firstName = user?.profile?.name?.firstName;

就这么简单!如果 userprofilename 任何一个不存在,整个表达式返回 undefined,而不是报错。

2.1 函数调用可选链

const result = api?.getData?.();

如果 apinull/undefined,或者 api.getData 不是函数,都返回 undefined,不会抛错。

2.2 数组元素可选链

const firstItem = arr?.[0];

如果 arr 不是数组或者是 null/undefined,返回 undefined

2.3 与空值合并搭配使用

const firstName = user?.profile?.name?.firstName ?? '匿名';

如果最终结果是 undefinednull,就换成默认值。完美!

三、实战对比:代码简洁度暴增

场景1:读取深层 API 响应

// 旧写法
const city = response && response.data && response.data.user && response.data.user.address && response.data.user.address.city;

// 新写法
const city = response?.data?.user?.address?.city;

场景2:调用可选回调

// 旧写法
if (onSuccess && typeof onSuccess === 'function') {
  onSuccess(data);
}

// 新写法
onSuccess?.(data);

场景3:动态属性名

const value = obj?.[key];

四、注意事项:别滥用

  • ?. 只检查左侧是否为 nullundefined,不检查 false0'' 等假值。如果你需要过滤假值,用 ||??
  • 不能用于赋值obj?.prop = value 是语法错误。
  • 短路效应:一旦遇到 null/undefined,右侧整个链停止求值,包括函数调用。
  • 性能:现代浏览器对 ?. 优化很好,放心用。

五、兼容性与降级

可选链是 ES2020 特性,现代浏览器都支持(Chrome 80+、Firefox 74+、Safari 13.1+)。如果需要兼容旧浏览器,可以用 Babel 插件 @babel/plugin-proposal-optional-chaining 转译。

六、总结:告别防御性编程噩梦

可选链操作符让 JavaScript 代码变得更加简洁、安全、可读。你不再需要写一长串 && 来保护每一层属性访问,也不用担心 Cannot read property of undefined 半夜叫醒你。

记住:?. 代替 && 链,用 ?? 提供默认值。这两个好基友,能让你的代码年轻十岁。


每日一问:你曾经因为忘记检查深层属性,导致过线上报错吗?或者写过最长的 && 链有多长?评论区晒出你的“防御塔”代码,让大家开开眼!

❌
❌