优化需以具体指标(如 LCP、FID)为导向,结合工具(如 Google PageSpeed Insights)检测瓶颈,核心手段包括:
-
按需加载资源
- 异步加载:非首屏 JS 使用
async/defer,组件采用异步加载(如 Vue 的() => import(...));
- 懒加载:图片、视频等非可视区资源通过
IntersectionObserver延迟加载;
- Polyfill 优化:使用
polyfill.io按需加载浏览器所需的 polyfill(仅加载当前环境缺失的部分)。
-
缩减资源体积
- 打包压缩:webpack 通过
TerserPlugin压缩 JS,css-minimizer-webpack-plugin压缩 CSS,Tree-Shaking 移除未使用代码;
- 传输压缩:服务端启用 gzip/brotli 压缩(文本类资源压缩率可达 60%+);
- 图片优化:
- 格式转换:优先使用 webp/avif(同等质量下体积比 jpg 小 30%+);
- 响应式图片:通过
srcset/sizes根据屏幕分辨率加载不同尺寸图片;
- 压缩处理:使用 tinypng 等工具压缩图片,去除元数据;
- 控制 Cookie 大小:Cookie 随每个请求发送,过大导致请求头臃肿,建议将非必要数据存 localStorage。
-
时序优化
- 并行请求:通过
Promise.all并发处理无依赖的接口请求,减少总耗时;
- 预加载策略:
preload:提前加载当前页面必需的关键资源(如字体、首屏图片);
prefetch:预加载可能在未来页面使用的资源(如跳转页的 JS);
dns-prefetch/preconnect:提前解析域名 DNS、建立 TCP 连接(如 CDN 域名);
- 服务端渲染(SSR):首屏内容由服务端直接生成 HTML,减少客户端渲染时间(如 Next.js、Nuxt.js)。
-
缓存策略
- CDN 缓存:静态资源(JS、CSS、图片)部署到 CDN,利用边缘节点加速访问;
- HTTP 缓存:通过
Cache-Control/Expires设置强缓存,ETag/Last-Modified设置协商缓存;
- 客户端存储:使用
localStorage/sessionStorage缓存不常变数据(如用户信息、字典表)。
核心思路:定位耗时函数,通过性能分析工具或代码埋点排查瓶颈。
- 装饰器法计算函数执行时间:
// 装饰器函数:计算目标函数执行时间
const measureTime = (fn) => {
return function (...args) {
const start = performance.now(); // 高精度时间戳
const result = fn.apply(this, args); // 执行原函数
const end = performance.now();
console.log(`${fn.name} 执行时间:${(end - start).toFixed(2)}ms`);
return result;
};
};
// 使用示例
const heavyTask = () => {
let sum = 0;
for (let i = 0; i < 10000000; i++) sum += i;
return sum;
};
const measuredTask = measureTime(heavyTask);
measuredTask(); // 输出:heavyTask 执行时间:XX.ms
- 进阶工具:浏览器 DevTools 的
Performance面板录制执行过程,定位长任务(Long Task);使用console.time/console.timeEnd快速标记代码块耗时。
实现思路:通过 OSS 链接参数指定格式,结合浏览器 webp 支持性判断。
// 判断浏览器是否支持webp
const supportWebp = async () => {
try {
const elem = document.createElement('canvas');
return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
} catch (e) {
return false;
}
};
// 转换图片为webp(若支持)
const convertToWebp = async (imgUrl) => {
if (!imgUrl) return imgUrl;
const isSupport = await supportWebp();
if (!isSupport) return imgUrl;
// 阿里云OSS通过参数指定格式:?x-oss-process=image/format,webp
return imgUrl.includes('?')
? `${imgUrl}&x-oss-process=image/format,webp`
: `${imgUrl}?x-oss-process=image/format,webp`;
};
注意事项:
- 需确认 OSS 已开启图片处理功能;
- 降级处理:对不支持 webp 的浏览器(如 IE)返回原图;
- 兼容性:部分老旧设备可能支持 webp 但存在渲染问题,建议测试验证。
实现思路:使用 Promise 控制并发数,每次仅加载指定数量的图片,完成后再加载下一批。
// 并发控制函数:限制同时执行的Promise数量
const limitConcurrency = (tasks, limit) => {
return new Promise((resolve) => {
let index = 0; // 当前任务索引
let completed = 0; // 已完成任务数
const results = []; // 结果存储
// 执行单个任务
const run = () => {
if (index >= tasks.length) return;
const task = tasks[index];
index++;
task()
.then((res) => {
results[index - 1] = res;
completed++;
if (completed === tasks.length) {
resolve(results); // 所有任务完成
} else {
run(); // 继续执行下一个任务
}
})
.catch((err) => {
results[index - 1] = err;
completed++;
run();
});
};
// 启动初始并发任务
for (let i = 0; i < limit; i++) {
run();
}
});
};
// 使用示例:加载图片,限制同时加载3张
const loadImages = (urls) => {
// 生成图片加载任务
const tasks = urls.map((url) => () =>
new Promise((resolve) => {
const img = new Image();
img.src = url;
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
})
);
// 限制并发数为3
return limitConcurrency(tasks, 3);
};
-
内存生命周期:
- 分配:声明变量、函数、对象时,JS 引擎自动分配内存(如
const obj = { a: 1 });
- 使用:读写内存(如访问对象属性、调用函数);
- 回收:垃圾回收机制自动回收不再使用的内存。
-
垃圾回收机制:
- 引用计数法:通过对象被引用的次数判断是否回收,存在循环引用缺陷(如两个对象相互引用但均无用时无法回收);
- 标记清除法:从全局对象出发,标记所有可达对象,未标记的对象被回收(解决循环引用问题,现代浏览器主流算法)。
-
常见内存泄露场景:
- 意外全局变量:未声明的变量(如
function fn() { a = 1 })挂载到window,不会被回收;
- 未清理的定时器 / 回调:
setInterval未被clearInterval清除,即使 DOM 已移除,定时器及内部依赖仍驻留内存;
- 闭包引用:闭包持有外部变量引用,若闭包未释放,变量无法回收(如反复调用的函数中闭包持有大对象);
- 未释放的 DOM 引用:移除 DOM 元素后,仍保留其引用(如
const el = document.getElementById('box'); document.removeChild(el);,el仍存在)。
-
避免内存泄露:
- 启用严格模式(
'use strict'),避免意外全局变量;
- 及时清理定时器(
clearInterval/clearTimeout)、事件监听(removeEventListener);
- 减少不必要的闭包引用,不再使用的变量手动赋值
null;
- 释放 DOM 引用:移除元素后,将对应变量置为
null。
思路:递归遍历对象,累加各类型值的字节数(粗略估算,JS 无原生 API 直接获取)。
const sizeOf = (obj) => {
const type = typeof obj;
// 基础类型
if (type === 'number' || type === 'boolean') return 8; // 数字/布尔占8字节
if (type === 'string') return obj.length * 2; // 字符串每个字符2字节
if (type === 'undefined') return 0;
if (obj === null) return 4;
// 引用类型:对象、数组、函数等
let bytes = 0;
const seen = new Set(); // 避免循环引用
const calculate = (item) => {
if (seen.has(item)) return; // 已计算过的对象跳过
seen.add(item);
if (typeof item === 'object') {
bytes += 16; // 对象头部开销约16字节
// 遍历对象属性
if (Array.isArray(item)) {
item.forEach(calculate);
} else {
Object.keys(item).forEach(key => {
bytes += key.length * 2; // 键名字节数
calculate(item[key]); // 递归计算值
});
}
} else if (typeof item === 'function') {
bytes += item.toString().length * 2; // 函数按字符串估算
}
};
calculate(obj);
return bytes;
};
跨域指浏览器因同源策略(协议、域名、端口一致)限制不同源的请求,解决方案包括:
- JSONP:利用
<script>标签无跨域限制,通过回调函数获取数据(仅支持 GET);
- CORS:服务端设置响应头
Access-Control-Allow-Origin等,允许指定源访问(支持所有 HTTP 方法);
- Node 正向代理:前端请求本地 Node 服务,Node 转发请求到目标服务器(利用服务端无跨域限制);
- Nginx 反向代理:通过
proxy_pass将跨域请求代理为同源请求(如location /api { proxy_pass https://target.com; });
- 图片 ping:通过
<img>标签发送 GET 请求(仅用于上报数据,无返回值)。
通过拦截器实现请求 / 响应的统一处理:
import axios from 'axios';
// 创建Axios实例
const instance = axios.create({ baseURL: '/api' });
// 请求拦截器:统一设置登录态
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
(err) => Promise.reject(err)
);
// 响应拦截器:统一错误处理
instance.interceptors.response.use(
(res) => res.data,
(err) => {
// 统一弹Toast提示错误
if (err.response) {
const { status } = err.response;
if (status === 401) {
// 未登录:跳转登录页
window.location.href = '/login';
} else {
alert(`错误:${err.response.data.msg || '请求失败'}`);
}
} else {
alert('网络异常,请重试');
}
return Promise.reject(err);
}
);
export default instance;
重写XMLHttpRequest原型,在生命周期阶段触发自定义回调:
// 保存原生XHR
const NativeXHR = window.XMLHttpRequest;
// 自定义XHR
window.XMLHttpRequest = function () {
const xhr = new NativeXHR();
const hooks = {
onreadystatechange: [],
onload: [],
onerror: [],
onprogress: []
};
// 重写事件处理函数
xhr.addEventListener = function (type, handler) {
if (hooks[type]) {
hooks[type].push(handler);
} else {
NativeXHR.prototype.addEventListener.call(xhr, type, handler);
}
};
// 监控readyState变化
const originalOnReadyStateChange = xhr.onreadystatechange;
xhr.onreadystatechange = function (...args) {
// 触发自定义hook
hooks.onreadystatechange.forEach(handler => handler.apply(xhr, args));
// 执行原生逻辑
if (originalOnReadyStateChange) originalOnReadyStateChange.apply(xhr, args);
};
// 监控其他事件(onload、onerror等)
['onload', 'onerror', 'onprogress'].forEach(event => {
Object.defineProperty(xhr, event, {
set(handler) {
hooks[event].push(handler);
}
});
});
// 初始化时打印日志
console.log('XHR初始化', xhr);
return xhr;
};
// 使用示例:在各阶段打印日志
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.addEventListener('readystatechange', () => {
console.log('readyState变化:', xhr.readyState);
});
xhr.addEventListener('load', () => {
console.log('请求完成:', xhr.responseText);
});
xhr.send();
发布订阅模式中,事件触发(emit)与回调执行默认是同步的(如 Node 的EventEmitter、Vue 的 Event Bus)。即调用emit后,会立即执行所有订阅的回调函数,执行完成后再继续后续代码。
class EventEmitter {
constructor() {
this.events = Object.create(null); // 存储事件:{ eventName: [fn1, fn2] }
}
// 订阅事件
on(eventName, handler) {
if (!this.events[eventName]) this.events[eventName] = [];
this.events[eventName].push(handler);
}
// 触发事件
emit(eventName, ...args) {
if (!this.events[eventName]) return;
// 复制一份避免执行中删除导致的问题
[...this.events[eventName]].forEach(handler => handler.apply(this, args));
}
// 只订阅一次
once(eventName, handler) {
const wrapper = (...args) => {
handler.apply(this, args);
this.off(eventName, wrapper); // 执行后取消订阅
};
this.on(eventName, wrapper);
}
// 取消订阅
off(eventName, handler) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(fn => fn !== handler);
}
}
// 使用示例
const emitter = new EventEmitter();
emitter.on('say', (msg) => console.log('say:', msg));
emitter.once('hello', () => console.log('hello once'));
emitter.emit('say', 'hi'); // 输出:say: hi
emitter.emit('hello'); // 输出:hello once
emitter.emit('hello'); // 无输出(已取消)
问题:给定容量为C的背包和n件物品(每件物品重量w[i],价值v[i]),每件物品仅能选一次,求背包能装的最大价值。
解法:
- 定义
dp[i][j]:前i件物品放入容量j的背包的最大价值;
- 状态转移:
- 不选第
i件:dp[i][j] = dp[i-1][j];
- 选第
i件(若j >= w[i]):dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i]);
- 优化:用一维数组
dp[j](滚动数组),空间复杂度从O(n*C)降至O(C)。
const knapsack = (C, weights, values) => {
const n = weights.length;
const dp = new Array(C + 1).fill(0); // dp[j]:容量j的最大价值
for (let i = 0; i < n; i++) {
// 倒序遍历避免重复使用同一物品
for (let j = C; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[C];
};
// 示例:背包容量5,物品[(2,3), (3,4), (4,5)]
console.log(knapsack(5, [2, 3, 4], [3, 4, 5])); // 输出:7(选重量2和3的物品,价值3+4=7)