总结-遇到
以下是关于「京东前端二面常考手撕题目」的整理,包括高频题型、示例解析和实现建议,帮助你高效准备第二轮面试。
一、高频手写题汇总
1. 实现发布–订阅模式(EventEmitter / Pub-Sub)
题目来源:京东前端二面常考题(必备)(腾讯云)
示例代码(ES6 Class 实现):
class EventEmitter {constructor() {this._events = {};}on(eventName, callback) {this._events[eventName] = [...(this._events[eventName] || []), callback];}emit(eventName, ...args) {(this._events[eventName] || []).forEach(fn => fn(...args));}off(eventName, callback) {if (!this._events[eventName]) return;this._events[eventName] = this._events[eventName].filter(fn => fn !== callback);if (this._events[eventName].length === 0) delete this._events[eventName];}once(eventName, callback) {const wrapper = (...args) => {callback(...args);this.off(eventName, wrapper);};this.on(eventName, wrapper);}
}
- 考点:事件管理、数组操作、对象存储结构、once 的实现技巧。
- 建议:实现 clean、完整的增删订阅/触发逻辑,额外加上
once
可加分。逻辑清晰、变量命名规范是加分项。
2. 实现 new
操作符机制
来源:同上列出的“常考手写题”(腾讯云)
实现思路:
- 创建一个空对象;
- 设置其原型为构造函数的
prototype
; - 绑定
this
并调用构造函数; - 判断构造函数的返回值类型:若为对象则返回,否则返回新对象。
示例代码:
function objectFactory(Constructor, ...args) {if (typeof Constructor !== 'function') {throw new TypeError('Constructor must be a function');}const obj = Object.create(Constructor.prototype);const result = Constructor.apply(obj, args);return result && (typeof result === 'object' || typeof result === 'function') ? result : obj;
}
- 考点:原型链、对象创建方式、类型判断与返回值处理。
3. 实现 Function.prototype.apply
来源:同上(腾讯云)
示例代码:
Function.prototype.myApply = function (ctx, args = []) {ctx = ctx || globalThis;const fnSymbol = Symbol();ctx[fnSymbol] = this;const result = ctx[fnSymbol](...args);delete ctx[fnSymbol];return result;
};
- 考点:上下文绑定、
this
指向、Symbol 避免命名冲突。
4. 防抖函数(Debounce)
来源:同上(腾讯云)
实现:
function debounce(fn, delay) {let timer = null;return function (...args) {clearTimeout(timer);timer = setTimeout(() => fn.apply(this, args), delay);};
}
- 考点:高频触发优化、定时器管理、闭包使用。
5. 判断对象是否有循环引用
来源:同上(腾讯云)
思路:使用 WeakMap
跟踪已访问对象,递归遍历属性进行检测。
6. 字符串 repeat
方法实现
来源:同上(腾讯云)
示例:
function repeat(str, n) {return new Array(n + 1).join(str);
}
或递归方式:
function repeat(str, n) {return n > 0 ? str + repeat(str, n - 1) : '';
}
7. 其他常见手写题
来自社区整理的题型还包括(来源云+掘金)(掘金):
- 实现
Promise
(手写 Promise 类或方法); - 实现
Object.is
; - 实现
const
/let
(限制作用域); - 实现
apply
、bind
、call
; - 实现
async/await
(用 Generator + Promise); - 其他经典设计模式如 发布–订阅、单例等。
二、京东前端二面常见问答题(补充)
常见渲染与 JS 基础题:
来自 LeetCode 讨论面经整理(LeetCode):
querySelector
是否可以选伪元素?- 异步定时器的精确性问题,如何解决?
- 数组去重策略,O(n) 实现,兼容不同类型(如
"1"
和1
); indexOf
原理,以及 ES6 中Set
、Map
应对 NaN 的不同;- 柯里化函数、统计执行次数等;
- 组件通信方式(Vue 的父孙、事件总线、Vuex 等);
- Vue 和 React 的对比;
- 性能优化如首屏加载、懒加载预加载等。
三、如何高效准备
类别 | 建议练习方式 |
---|---|
手写题 | 按模块整理,高频先攻(EventEmitter、new、apply、Promise) |
面试问答 | 做笔记 + 理解原理,练习面向面试官讲清楚(比如异步定时器为什么不准?解释 JS 内部) |
代码规范 | 注重处理边界情况、异常、安全写法(比如使用 Symbol 避免 key 冲突) |
四、下一步建议
如果你需要,我可以帮你:
- 为上述某个题目提供完整、注释丰富的版本;
- 把 Promise / async-await / 发布-订阅模式写成通用工具包;
- 编写“面试答题模板”——包括结构、常见追问方向与回答思路。
你也可以告诉我你最想优先准备哪类题(比如手写 Promise 或数组去重),我可以为你打磨一个“完整”版本。
期待你的指令,我随时可以帮你深挖某道题目并给出精炼、易读、面试友好的实现!
好——一次性把你要的手撕题目都给到:实现代码 + 原理要点 + 常见坑与面试回答要点。每一节我都附上可直接复制运行的代码样例与简短测试,便于面试现场“写完能跑得通并解释清楚”。
1 实现一个完整(且尽量规范)的 Promise(手写 Promise 类)
要点复述(面试要点):
- 状态机(PENDING / FULFILLED / REJECTED)
- then 链式、异步(microtask)执行回调
- 采用 Promise Resolution Procedure(处理 thenable)避免循环与重复 resolve/reject
- 提供静态方法(resolve/reject/all/race)与 finally
完整实现(尽量贴近规范但为教学简化):
// MyPromise.js
class MyPromise {static PENDING = 'PENDING';static FULFILLED = 'FULFILLED';static REJECTED = 'REJECTED';constructor(executor) {this.state = MyPromise.PENDING;this.value = undefined;this.handlers = []; // {onFulfilled, onRejected, resolve, reject}const resolve = (value) => {this._resolvePromise(value);};const reject = (reason) => {if (this.state !== MyPromise.PENDING) return;this.state = MyPromise.REJECTED;this.value = reason;this._queueHandlers();};try {executor(resolve, reject);} catch (err) {reject(err);}}// ensure async task scheduling (microtask)static _queueMicrotask(fn) {if (typeof queueMicrotask === 'function') return queueMicrotask(fn);// fallbackreturn Promise.resolve().then(fn);}_resolvePromise(x) {if (this.state !== MyPromise.PENDING) return;// Promise Resolution Procedureconst resolveProcedure = (promise, x) => {if (promise === x) {return promise._doReject(new TypeError('Chaining cycle detected for promise'));}if (x && (typeof x === 'object' || typeof x === 'function')) {let then;let called = false;try {then = x.then;if (typeof then === 'function') {then.call(x,(y) => {if (called) return;called = true;resolveProcedure(promise, y);},(r) => {if (called) return;called = true;promise._doReject(r);});return;}} catch (err) {if (!called) return promise._doReject(err);}}// not thenablepromise._doFulfill(x);};resolveProcedure(this, x);}_doFulfill(value) {if (this.state !== MyPromise.PENDING) return;this.state = MyPromise.FULFILLED;this.value = value;this._queueHandlers();}_doReject(reason) {if (this.state !== MyPromise.PENDING) return;this.state = MyPromise.REJECTED;this.value = reason;this._queueHandlers();}_queueHandlers() {MyPromise._queueMicrotask(() => {while (this.handlers.length) {const { onFulfilled, onRejected, resolve, reject } = this.handlers.shift();try {if (this.state === MyPromise.FULFILLED) {if (typeof onFulfilled === 'function') {const res = onFulfilled(this.value);resolve(res);} else {resolve(this.value);}} else if (this.state === MyPromise.REJECTED) {if (typeof onRejected === 'function') {const res = onRejected(this.value);resolve(res);} else {reject(this.value);}}} catch (err) {reject(err);}}});}then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {this.handlers.push({onFulfilled,onRejected,resolve,reject});// If already settled, ensure handlers run (they will via _queueHandlers)if (this.state !== MyPromise.PENDING) {this._queueHandlers();}});}catch(onRejected) {return this.then(undefined, onRejected);}finally(onFinally) {return this.then((v) => MyPromise.resolve(onFinally && onFinally()).then(() => v),(e) => MyPromise.resolve(onFinally && onFinally()).then(() => { throw e; }));}// static helpersstatic resolve(value) {if (value instanceof MyPromise) return value;return new MyPromise((resolve) => resolve(value));}static reject(reason) {return new MyPromise((_, reject) => reject(reason));}static all(iterable) {return new MyPromise((resolve, reject) => {const arr = Array.from(iterable);const res = [];let count = 0;if (arr.length === 0) return resolve([]);arr.forEach((p, i) => {MyPromise.resolve(p).then((v) => {res[i] = v;count++;if (count === arr.length) resolve(res);},(err) => reject(err));});});}static race(iterable) {return new MyPromise((resolve, reject) => {for (const p of iterable) {MyPromise.resolve(p).then(resolve, reject);}});}
}// --- 简单测试
const p = new MyPromise((res) => res(1));
p.then(v => v + 1).then(console.log); // 2
MyPromise.all([1, Promise.resolve(2), MyPromise.resolve(3)]).then(console.log); // [1,2,3]
面试讲点:
- 一定强调「microtask」而不是
setTimeout
(因为 then 的回调必须在当前执行栈结束后立即异步执行,优先于 macrotask)。 - 一定说明处理 thenable(
then
方法)及防止循环引用(if (promise === x)
抛错)。 - 常见坑:重复 resolve(用
called
guard)、executor 抛错要reject
。
2 实现 Object.is
(注意 +0/-0 与 NaN)
实现非常短,面试时顺带说明 ===
与 Object.is
的区别:
===
将 +0 和 -0 视为相等(true),Object.is
将 +0 与 -0 区分。===
把 NaN 与 NaN 视为不相等(false),Object.is
视为相等。
实现:
function myObjectIs(x, y) {if (x === y) {// covers non-zero equal values but +0 === -0 (true) -> need to distinguishreturn x !== 0 || 1 / x === 1 / y;}// handle NaNreturn x !== x && y !== y;
}// 测试
console.log(myObjectIs(NaN, NaN)); // true
console.log(myObjectIs(+0, -0)); // false
console.log(myObjectIs(1, 1)); // true
面试点:要解释 1/x
trick 用来区分 +0 和 -0;NaN 的判断用 x !== x
。
3 实现 call
/ apply
/ bind
常见要求:实现基础功能 + bind 可被 new 使用(构造时保留原型链行为)。重点讲清楚用 Symbol 防止属性冲突、处理 null/undefined 指向 globalThis。
// call
Function.prototype.myCall = function(ctx, ...args) {ctx = ctx == null ? globalThis : Object(ctx);const fnKey = Symbol('fn');ctx[fnKey] = this;const result = ctx[fnKey](...args);delete ctx[fnKey];return result;
};// apply
Function.prototype.myApply = function(ctx, args) {ctx = ctx == null ? globalThis : Object(ctx);const fnKey = Symbol('fn');ctx[fnKey] = this;const result = Array.isArray(args) ? ctx[fnKey](...args) : ctx[fnKey]();delete ctx[fnKey];return result;
};// bind (supports new)
Function.prototype.myBind = function(ctx, ...bindArgs) {const fn = this;function bound(...args) {const isNew = this instanceof bound;const thisArg = isNew ? this : ctx;return fn.apply(thisArg, bindArgs.concat(args));}// preserve prototype chain for `new bound()`bound.prototype = Object.create(fn.prototype);return bound;
};// --- 测试
function Foo(a, b) {this.sum = (a || 0) + (b || 0);
}
const ctx = { x: 1 };
function test(c) { return [this, this.x, c]; }console.log(test.myCall(ctx, 3)); // [ctx, 1, 3]
console.log(test.myApply(ctx, [4])); // [ctx, 1, 4]const Bound = Foo.myBind({}, 2);
const inst = new Bound(3);
console.log(inst.sum); // 5
面试点:
bind
被new
使用时,应忽略绑定的 this,返回新构造的实例(因此this instanceof bound
判断常用)。- 使用
Symbol
避免属性覆盖;对null/undefined
传入会落到globalThis
。 - 注意函数的 prototype 问题(bound.prototype = Object.create(fn.prototype))。
4 实现 async/await
(用 Generator + Promise)—— async
的“polyfill”/实现思路
思路:async function
本质上是把 generator 与 Promise runner 结合:generator 内每个 yield
对应一个 await
,外面的 runner 每次把 yield
出来的值 Promise.resolve()
后再 .then()
驱动 generator。
实现一个 asyncToGenerator
工具:
function asyncToGenerator(genFn) {return function(...args) {const gen = genFn.apply(this, args);return new Promise((resolve, reject) => {function step(key, arg) {let info;try {info = gen[key](arg);} catch (err) {return reject(err);}const { value, done } = info;if (done) {return resolve(value);} else {return Promise.resolve(value).then((v) => step('next', v),(e) => step('throw', e));}}step('next');});};
}// 使用示例(generator 形式)
const fetch = (v) => new Promise(res => setTimeout(()=>res(v), 10));
const asyncFn = asyncToGenerator(function* (x) {const a = yield fetch(1);const b = yield fetch(a + x);return b + 1;
});asyncFn(2).then(console.log); // 4
面试点:
- 解释如何把
yield
和await
对应起来(yield
暴露控制权,runner 把yield
的值统一 Promise.resolve)。 - 错误处理要把
.then
的 reject 对应到generator.throw
,即step('throw', err)
。 - 可以提到 Babel/Regenerator 的做法。
5 实现 const/let
(限制作用域)
注意:在真实 JS 引擎中 let/const
是由编译器/引擎实现的,手写的 JS 无法完全改变宿主语法。面试常问的实现题通常是:如何在旧环境仿真 block scope 或者让你写一个简单的 transform 思路。下面给出 三种常见演示方案(面试讲解时选一种深究即可):
方法 A:用 IIFE(最常用的手工写法)
把块用函数封装。面试时手写一个例子说明原理:
// let a = 1; { let a = 2; console.log(a); } console.log(a);
// 转换成(手工):
let a = 1;
(function() {let a = 2;console.log(a); // 2
})();
console.log(a); // 1
解释:IIFE 提供函数作用域,实现了块作用域隔离。
方法 B:用闭包工厂按块生成变量(动态)
给出一个简单 runtime scope 模拟器(仅示意,不是实际替换 JS 语法):
function createBlockScope(fn) {// 每个 block 都得到一个独立的闭包作用域return (function() {const scope = {};return fn(scope);})();
}// 使用
createBlockScope(function(local) {local.x = 2;console.log(local.x);
});
面试点:说明 let/const
的 TDZ(Temporal Dead Zone)在 IIFE/closure 中难以完全模拟;const 的不可重新赋值等可以通过 Object.defineProperty
做不可写属性,但复杂性较高。
方法 C:简单的“转译器”示意(regex 版,仅教育意义)
把 let x
/const y
全替换成 var x_uniqueId
并包一层函数(注意:真实变换要用 parser,如 Babel)。这里只做非常简单的示意:
function naiveTranspileBlockScopes(code) {// extremely naive and unsafe: just demolet uid = 0;return code.replace(/\b(let|const)\s+([a-zA-Z_$][\w$]*)/g, (m, decl, name) => {uid++;return `var ${name}__$${uid}`;});
}
const src = "let a=1; { let b=2; console.log(b); }";
console.log(naiveTranspileBlockScopes(src));
面试点讲法:
- 在真实工程中用 Babel 做转换(@babel/parser + @babel/traverse + @babel/generator),把
BlockStatement
的let/const
变成函数作用域的var
并做变量重命名(alpha-renaming)防止冲突。 - 说清楚 Temporal Dead Zone(在声明前访问会抛
ReferenceError
),这是最难模拟的行为。
6 其他经典设计模式:发布–订阅(Pub/Sub)与单例
这部分通常面试会问实现并说明应用场景、优缺点。
发布–订阅(EventEmitter)
实现要点:on
、off
、once
、emit
,返回 unsubscribe 函数是常见友好 API。
class EventEmitter {constructor() {this._events = Object.create(null);}on(evt, handler) {(this._events[evt] || (this._events[evt] = [])).push(handler);// 返回 unsubscribereturn () => this.off(evt, handler);}off(evt, handler) {const list = this._events[evt];if (!list) return;this._events[evt] = list.filter(h => h !== handler);}once(evt, handler) {const wrapper = (...args) => {handler(...args);this.off(evt, wrapper);};this.on(evt, wrapper);}emit(evt, ...args) {(this._events[evt] || []).slice().forEach(h => {try { h(...args); } catch (e) { console.error(e); }});}
}// 测试
const bus = new EventEmitter();
const un = bus.on('x', v => console.log('x', v));
bus.emit('x', 1); // x 1
un();
bus.emit('x', 2); // nothing
面试点:
- 讨论内存泄漏(长时间订阅未取消)、并发发射时对数组的浅拷贝(
.slice()
),避免订阅列表被修改影响当前 emit。 - 举例场景:组件间松耦合通信、跨模块事件广播、日志统计。
单例(Singleton)
两种常用实现方式:基于类的懒实例与模块导出(ESM 本身天然单例)。
类版(懒加载):
class Singleton {constructor(name) {if (Singleton._instance) return Singleton._instance;this.name = name;Singleton._instance = this;}static getInstance(name) {return Singleton._instance || new Singleton(name);}
}// 测试
const s1 = Singleton.getInstance('a');
const s2 = new Singleton('b');
console.log(s1 === s2, s2.name); // true, 'a'
模块版(ESM):
// logger.js
class Logger { /* ... */ }
export default new Logger(); // 导入处都是同一实例
面试点:
- 讨论单例带来的全局状态问题、测试难度、不利于依赖注入;并说明什么时候用(配置、单一 DB 连接管理等)。
7 面试现场写题 & 思路建议(tips)
- 写代码要稳:先写最简单版本(通过基本用例),再补边界(thenable、重复 resolve、symbol key、new 操作)。面试官更看你对边界的认知。
- 说清楚异步模型:Promise 要提 microtask、thenable 处理、避免栈溢出;bind 要说 new 的行为;async->generator 要讲 error 路径。
- 不要盲改全局:实现 call/apply/bind 时用 Symbol 生成唯一 key。
- 测试样例:写 2-3 个简单 test case 说明你的实现满足常见场景(同步、异步、异常、构造)。
- 复杂实现可分层:比如 Promise:先实现基本 then/resolve/reject,再做 thenable/adopt 再做静态方法/finally。把实现拆解讲给面试官听会加分。
如果你想,我可以:
- 把
MyPromise
改成支持allSettled/any
或更贴近 ECMAScript 完整规范的版本(包括 unhandled rejection 的处理)。 - 把上面某一题现场“考官风格”抽题(比如给你一个半成品,你补完
thenable
处理),我给你带注释的逐步修改版本,方便你练习口述实现细节。
想先要哪一个 deeper 练习(比如 Promise 的 thenable/cycle 部分拆解,或 bind 的 new
场景单步演示)?
querySelector 是否可以选伪元素?
querySelector
不能直接选取伪元素。伪元素(比如 ::before
、::after
、::first-line
等)不是 DOM 树的一部分,它们是 CSS 引擎在渲染时,根据特定的规则动态生成的装饰性内容或结构。因此,JavaScript 的 DOM API,包括 querySelector
,无法直接访问和操作它们。
如果你需要修改伪元素,只能通过操作父元素的 class 或 style 来间接实现。例如,给父元素添加一个 class,然后在 CSS 中定义这个 class 状态下伪元素的新样式。
异步定时器的精确性问题,如何解决?
JavaScript 的异步定时器(setTimeout
和 setInterval
)不保证精确性。这是因为 JavaScript 是单线程的,所有任务都在主线程中执行。定时器只是将一个任务添加到任务队列(Task Queue)中。当主线程空闲时,事件循环(Event Loop)才会把任务队列中的任务取出来执行。如果主线程被其他耗时任务阻塞,定时器任务就会延迟执行。
解决定时器不精确的问题,通常可以采用递推或递归的方式,而不是简单的循环。例如,通过计算每次执行的时间差,来调整下一次定时器的时间。
// 模拟一个更精确的定时器
function accurateInterval(callback, interval) {let timerId,startTime = Date.now(),expectedTime = startTime + interval;function step() {const currentTime = Date.now();const drift = currentTime - expectedTime; // 计算时间偏移量callback();// 递推,调整下一次执行时间expectedTime += interval;// 递归调用,并考虑偏移量timerId = setTimeout(step, Math.max(0, interval - drift));}timerId = setTimeout(step, interval);return () => clearTimeout(timerId);
}// 使用
const stop = accurateInterval(() => {console.log("执行了!", Date.now());
}, 1000);// 5秒后停止
setTimeout(() => {stop();
}, 5000);
数组去重策略
数组去重有很多种方法,这里介绍一种 O(n) 的实现,并且能兼容不同类型(如 “1” 和 1)。核心思想是利用哈希表(Map 或 Object)来记录已经出现过的元素。
function uniqueArray(arr) {const seen = new Map(); // 使用 Map 兼容不同类型const result = [];for (let i = 0; i < arr.length; i++) {const item = arr[i];// Map 的 key 可以是任何类型,所以 "1" 和 1 是不同的 keyif (!seen.has(item)) {seen.set(item, true);result.push(item);}}return result;
}// 测试
const arr = [1,"1",2,2,"a","b","a",1,"b",1,NaN,NaN,undefined,null,
];
console.log(uniqueArray(arr)); // [ 1, '1', 2, 'a', 'b', NaN, undefined, null ]
indexOf 原理和 ES6 中 Set、Map 对 NaN 的不同
-
indexOf
原理:
indexOf
方法用于返回数组中某个元素第一次出现的索引。它的内部实现是进行严格相等比较 (===
)。这意味着indexOf
无法找到NaN
,因为NaN === NaN
永远为false
。 -
Set
、Map
应对NaN
的不同:
ES6 的Set
和Map
在处理NaN
时有特殊的规则:它们将所有NaN
都视为同一个值。Set
: 当你向Set
中添加多个NaN
时,只会保留一个。Map
: 当你使用NaN
作为Map
的键时,多次设置会覆盖同一个键值对。
// Set 示例
const mySet = new Set();
mySet.add(NaN);
mySet.add(NaN);
console.log(mySet.size); // 1// Map 示例
const myMap = new Map();
myMap.set(NaN, "value1");
myMap.set(NaN, "value2");
console.log(myMap.get(NaN)); // value2
柯里化函数和统计执行次数
柯里化(Currying) 是一种将接收多个参数的函数转换为一系列只接收单个参数的函数的技术。
// 柯里化函数示例
function curry(fn) {return function curried(...args) {// 参数不够,返回一个新函数继续接收参数if (args.length >= fn.length) {return fn(...args);} else {// 递归返回新函数return function (...nextArgs) {return curried(...args, ...nextArgs);};}};
}// 原始函数
function add(a, b, c) {return a + b + c;
}const curriedAdd = curry(add);console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
组件通信方式
-
Vue 的父子孙组件通信:
- 父传子: 使用
props
。父组件通过在子组件标签上绑定属性来传递数据。 - 子传父: 使用
$emit
。子组件通过$emit
触发一个自定义事件,父组件通过@
监听该事件并执行相应方法。 - 跨级组件(父孙):
props
逐级传递: 冗长且繁琐。$attrs
: 包含了父组件传入但子组件未被props
接收的属性,可用于向下传递。$listeners
: 包含了父组件传入但子组件未被emit
接收的事件监听器,Vue 3 中已移除。provide
/inject
: 提供了祖先组件和后代组件之间的依赖注入。祖先组件提供(provide
)数据,后代组件注入(inject
)使用。
- 父传子: 使用
-
事件总线 (
EventBus
):
创建一个空的 Vue 实例作为事件中心。一个组件通过$emit
触发事件,另一个组件通过$on
监听事件。适用于任意组件之间的通信,但在大型应用中不易维护。 -
Vuex/Pinia:
- 状态管理库,用于集中管理所有组件的状态。
- 适用于大型复杂的应用,可以解决任意组件之间的通信和共享状态问题。
- 核心概念:
State
(状态)、Mutations
(同步修改状态)、Actions
(异步提交Mutations
)、Getters
(派生状态)。
Vue 和 React 的对比
特性 | Vue | React |
---|---|---|
核心 | 渐进式框架 | 视图库 |
模板 | 单文件组件 (.vue ),template 模板 | JSX (JavaScript + XML) |
响应式 | 基于 Object.defineProperty 或 Proxy 自动追踪依赖 | 基于 setState 手动触发更新 |
状态管理 | 官方推荐 Vuex/Pinia | 社区生态,如 Redux、Zustand、MobX 等 |
生态 | 官方工具链完善,如 Vue CLI、Vite | 社区主导,如 Create React App、Next.js |
性能优化
-
首屏加载优化:
- 路由懒加载: 将不同路由对应的组件分割成不同的 JS 文件,按需加载。
- 代码分割 (Code Splitting): 将代码分割成更小的块,例如将第三方库单独打包。
- 图片优化: 压缩图片、使用 WebP 格式、CDN 加速。
- 服务端渲染 (SSR) 或预渲染 (Prerendering): 在服务器端生成 HTML,直接返回给浏览器,减少白屏时间。
- Gzip 压缩: 开启服务器 Gzip 压缩,减少文件传输大小。
- HTTP/2: 多路复用,加快资源加载。
-
懒加载/预加载:
- 懒加载 (Lazy Loading): 当用户滚动到视口时,才加载图片或其他资源。通常用于长列表或图片墙。
- 预加载 (Preloading): 在浏览器空闲时,提前加载用户将来可能会访问的资源,如下一页的组件或图片,从而提升后续的加载速度。
希望以上解释对你有帮助!你还想深入了解其中哪一部分呢?