html
html5 的新特性
| 标签 | 含义与使用场景 |
|---|---|
| header | 页面或者区域头部,通常包括标题,logo,导航栏 |
| footer | 页面或者区域的底部,通常包括版权、联系方式、相关链接 |
| nav | 导航栏区域,用于包裹住要的导航链接(主导航、侧边栏导航—) |
| article | 独立完整的“文章类”内容(博客、新闻、评论)可单独存在并被理解 |
| section | 页面中的“区块”(用于对内容分组),通常包含一个标题(h1-h6)强调“主题性” |
| aside | 侧边栏或“辅助内容”(如文章的相关推荐、作者信息、广告),与主内容相关但非必需 |
媒体标签
| 标签 | 含义与使用场景 |
|---|---|
| video | src/loop/autoplay/poster |
| audio | 嵌入音频,核心属性与<video>类似(无 poster),用于播放音乐、语音等 |
| source | 为<video/>/<audio/>提供 “多格式源”(浏览器会自动选择支持的格式),例:html<br><video controls><br> <source src="video.mp4" type="video/mp4"><br> <source src="video.webm" type="video/webm"><br></video><br> |
其他常用标签
| 标签 | 含义与使用场景 |
|---|---|
| canvas | 绘图画布(2D/3D),需通过 JS 操作(如绘制图形、动画、游戏),本身无内容,依赖脚本 |
| svg | 矢量图形标签(可直接嵌入 HTML),用于绘制图标、图表等(放大不失真) |
| details | 可折叠 / 展开的 “详情区域”,默认折叠,配合<summary>作为标题: |
| mark | 高亮显示文本(如搜索结果中的关键词),视觉上默认黄色背景 |
| time | 表示时间 / 日期,datetime 属性存储机器可读格式(利于 SEO):<time datetime="2025-07-24">今天</time> |
为什么用语义化标签
- 代码可读性、SEO 友好(搜索引擎更易理解内容结构),便于屏幕阅读器(无障碍访问),减少冗余 class 命名
SEO,需要知道的细节
优化思路
- 页面结构优化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="app,理财" />
<title>Document</title>
</head>
<body>
<header>Main page</header>
<nav>
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</nav>
<main>
<article>
<h1>标题</h1>
<p>内容</p>
</article>
</main>
<footer>底部版权说明,信息说明</footer>
</body>
</html>内容优化 保证页面中关键词的覆盖率
技术向SEO优化
- 地图
- 结构化数据
- 移动端兼容
@media (max-width : 600px) {
body{
}
}history,vue 路由的 hash 模式和 history 有什么区别?动态路由?按需加载?鉴权?
history 是 h5 提供的历史记录管理 api,允许 javascript 操作浏览器的会话历史,实现不刷新页面的情况下修改 URL,添加/替换历史记录功能。 hash 是页面的锚点
| 区别 | hash | history |
|---|---|---|
| URL 格式 | # | / |
| 是否需要服务端支持 | 不需要 | 需要(访问不存在页面,会报 404) |
| 底层原理 | 监听 hash change | 基于 h5 的 pushState |
| 兼容性 | 全部兼容 | 依赖 h5 ie>10+ |
| SEO 友好性 | 较差 | 较好(URL 更规范) |
| api | xx | -pushState -replaceState -go -back -forward |
webworker 和 wesocket
webwworker
是 H5 提供的一种多线程解决方案,主线程创建后台线程,在不阻塞主线程的情况下执行耗时任务(复杂计算、大数据处理)。 特点:1.线程隔离;与主线程运行在不同的上下文(self),不能直接操作 dom 和主线程的全局变量。2.通讯机制,worker.poseMessage() worker.onmessage 注意:数据是通过负责传递,而非共享。3.限制不能访问 dom、执行 alert、遵循同源策略(协议、域名、端口)
ServiceWorker
运行在浏览器后台的独立线程,与网页无关。本质是个代理服务器。主要用于实现 PWA。离线缓存、消息推送、后台同步等功能。是 PWA 的核心技术之一。 特点:1.生命周期独立。与网页无关,关了也能运行。2.离线缓存:拦截网络请求,优先读取缓存策略。3.通讯机制postMessage。4.安全机制:必须运行在 https 环境,遵循同源策略。
wesocket 是一种全双工通信协议,允许客户端与服务器之前建立持久连接,实现双方实时双工通信。打破 Http 的一问一答,适用需要实时数据交互的场景(IM,实时通知、在线协作工具)
特点:1.持久连接。无需重复建立连接。2.全双工通信,双方实时传输。3.低开销,握手协议基于 http 协议,后续通信不携带冗余头部消息。4.跨域支持.acess-control-allow-origin;5.二进制传输。不仅传输文本 UTF-8,还能传输二进制图片、视频。
工作流程
握手阶段:client 请求头带上Upgrade:wesocket Connection:Upgrade 表示升级为 wb 协议。service 收到后回复 101 switch protocol响应,握手成功。 通讯阶段:双方通过 帧格式传输数据。【操作码(文本/二进制)】【数据长度】 关闭阶段:发送关闭帧,对方确认后关闭。
与 Http 的区别
| 对比维度 | http | websocket |
|---|---|---|
| 连接方式 | 短连接 | 长连接 |
| 方向 | 单 | 双 |
| 头部开销 | 每次请求携带完整头部 | 握手后无冗余头部,开销低 |
| 场景 | 网页请求 | 聊天、实时传输 |
| 协议表示 | https:// | wss:// |
websocket 和 SSE 的区别
| 维度 | WebSocket | SSE |
|---|---|---|
| 方向 | 全双工 | 单项 |
| 协议基础 | 独立协议(ws://) | 基于http/https |
| 数据格式 | 文本、二进制(格式有应用层定义) | 仅支持文本(固定格式data:xxx\n\n |
| 连接限制 | 无浏览器同域名并发链接数 | 受浏览器同域名 HTTP 并发连接数限制(通常为 6 个) |
| 自动重连 | 应用层实现 | 浏览器原生自动重连 |
| 数据传输效率 | 连接后无 http 头,效率高 | 基于 http 持久连接,每个消息有少量头部传递 |
| 应用场景 | 高频交互(在线游戏、聊天、协同) | 单项(股票、推送、通知、新闻) |
为什么ai应用常用sse而不是socket
- 架构匹配:AI交互本质上请求-响应,不是真的双向对话
- 开发简单:SSE API简单,自动处理重连
- 生态兼容:基于HTTP,无缝集成现有基础设施
- 成本效益:对于ai流式输出场景,SSE已完全足够。
sse断线自动重连,如何捞回丢失数据
| 方案 | 描述 | 适合场景 |
|---|---|---|
| 消息id + Last-Event-ID | 服务端每条消息都带ID,客户端重连携带最后收到的ID | 简单消息流 |
| 检查点机制 | 客户端定期确认收到位置 | 大文件下载 |
| 服务端缓冲 | 服务器保存最近N条消息 | 聊天应用 |
| 混合方案 | 以上组合 | ai流式输出 |
// last-event-id + 服务端缓存消息
let messageCounter = 0;
const messageBuffer = new Map(); // 缓存最近的消息
app.get('/api/ai-stream', (req, res) => {
// 获取客户端最后收到的消息ID
const lastEventId = req.headers['last-event-id'] || '0';
console.log('客户端最后收到的ID:', lastEventId);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// 发送一个心跳,保持连接
const heartbeatInterval = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
// 模拟AI流式输出
async function streamAIResponse(prompt, startFromId = 0) {
const aiResponse = "这是AI生成的流式回复内容,会分成多个chunk发送。";
const chunkSize = 3;
for (let i = 0; i < aiResponse.length; i += chunkSize) {
const chunk = aiResponse.substring(i, Math.min(i + chunkSize, aiResponse.length));
messageCounter++;
// 缓存消息
messageBuffer.set(messageCounter.toString(), {
id: messageCounter,
data: chunk,
timestamp: Date.now()
});
// 清理旧缓存(最多保存1000条)
if (messageBuffer.size > 1000) {
const oldestKey = Array.from(messageBuffer.keys())[0];
messageBuffer.delete(oldestKey);
}
// 发送消息,包含id
res.write(`id: ${messageCounter}\n`);
res.write(`data: ${JSON.stringify({ chunk, index: i })}\n\n`);
await new Promise(resolve => setTimeout(resolve, 50));
}
// 发送结束标记
messageCounter++;
res.write(`id: ${messageCounter}\n`);
res.write(`event: end\n`);
res.write(`data: ${JSON.stringify({ status: 'complete' })}\n\n`);
}
// 判断是否需要重发丢失的消息
const lastIdNum = parseInt(lastEventId);
if (lastIdNum > 0) {
// 重发丢失的消息
const lostMessages = [];
for (let id = lastIdNum + 1; id <= messageCounter; id++) {
const cached = messageBuffer.get(id.toString());
if (cached) {
lostMessages.push(cached);
}
}
// 先重发丢失的消息
lostMessages.forEach(msg => {
res.write(`id: ${msg.id}\n`);
res.write(`data: ${JSON.stringify({
chunk: msg.data,
isRecovery: true, // 标记为重发数据
originalTimestamp: msg.timestamp
})}\n\n`);
});
}
// 开始新的流式输出
streamAIResponse("用户的问题");
// 清理
req.on('close', () => {
clearInterval(heartbeatInterval);
});
});// 检查点机制
// 客户端实现检查点
class SSEClientWithCheckpoint {
constructor(url, options = {}) {
this.url = url;
this.checkpointInterval = options.checkpointInterval || 5000; // 5秒确认一次
this.lastCheckpointId = localStorage.getItem('lastCheckpointId') || '0';
this.pendingMessages = new Map(); // 未确认的消息
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.connect();
}
connect() {
// 在URL中传递最后确认的消息ID
const urlWithCheckpoint = `${this.url}?lastCheckpoint=${this.lastCheckpointId}`;
this.eventSource = new EventSource(urlWithCheckpoint);
this.eventSource.onmessage = this.handleMessage.bind(this);
this.eventSource.onerror = this.handleError.bind(this);
// 定期发送确认
this.checkpointTimer = setInterval(() => {
this.sendCheckpoint();
}, this.checkpointInterval);
}
handleMessage(event) {
const data = JSON.parse(event.data);
const messageId = event.lastEventId || data.id;
if (!messageId) return;
// 存储未确认的消息
this.pendingMessages.set(messageId, {
data: data,
timestamp: Date.now()
});
// 处理消息
this.onMessage(data, messageId);
// 立即确认(或批量确认)
if (data.isCritical) {
this.sendCheckpoint(messageId);
}
}
sendCheckpoint(lastConfirmedId = null) {
if (!lastConfirmedId) {
// 找出已处理的最大ID
const ids = Array.from(this.pendingMessages.keys())
.map(id => parseInt(id))
.filter(id => !isNaN(id));
if (ids.length === 0) return;
lastConfirmedId = Math.max(...ids).toString();
}
// 发送确认到服务器
fetch(`${this.url}/checkpoint`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: this.clientId,
lastCheckpointId: lastConfirmedId
}),
keepalive: true // 即使页面关闭也发送
}).then(() => {
this.lastCheckpointId = lastConfirmedId;
localStorage.setItem('lastCheckpointId', lastConfirmedId);
// 清理已确认的消息
this.pendingMessages.forEach((msg, id) => {
if (parseInt(id) <= parseInt(lastConfirmedId)) {
this.pendingMessages.delete(id);
}
});
});
}
handleError() {
this.reconnectAttempts++;
if (this.reconnectAttempts > this.maxReconnectAttempts) {
console.error('达到最大重连次数');
clearInterval(this.checkpointTimer);
this.eventSource.close();
return;
}
// 指数退避重连
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => {
this.eventSource.close();
this.connect();
}, delay);
}
onMessage(data, messageId) {
// 由用户实现
console.log('收到消息:', data, 'ID:', messageId);
}
}// 会话状态恢复(AI应用专用)应用
解决socket断联问题
- 心跳检测。客户端或者服务端主动发送一个空包,对端同样返回相同空包,保障连接的稳定性和持久性。
- 流程:每30s发送心跳包
heartbeat,后端立即响应,前端15s内未收到,则标记为异常。后端每30s检测客户端最后存活时间,若60s内未收到心跳,则主动断开连接。
- 流程:每30s发送心跳包
- 自动重连。客户端监测到断开,可采用指数退避的方式实现自动重连。
重连设计:
- 自动重连触发:报错重连
attemptReconnect - 重连策略:重连次数限制,重连时间递增
1s 每次乘1.5s 最大不超过10s - 连接状态管理:标记
isConnected,重连前清空旧资源 - 提供事件回调
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url; // WebSocket 服务地址(如 ws://localhost:8080)
this.ws = null; // WebSocket 实例
this.isConnected = false; // 连接状态
// 重连配置(默认值)
this.options = {
maxReconnectAttempts: 10, // 最大重连次数(-1 表示无限重试)
initialReconnectDelay: 1000, // 初始重连延迟(毫秒)
maxReconnectDelay: 10000, // 最大重连延迟(毫秒)
reconnectDelayGrowth: 1.5, // 延迟递增系数(每次重试延迟 = 上一次 * 系数)
...options,
};
this.reconnectAttempts = 0; // 当前重连次数
this.reconnectTimer = null; // 重连定时器
// 绑定事件回调(用户可通过 onXXX 方法注册)
this.onOpen = () => {}; // 连接成功回调
this.onMessage = (event) => {}; // 接收消息回调
this.onError = (error) => {}; // 错误回调
this.onClose = (code, reason) => {}; // 关闭回调
this.onMaxReconnect = () => {}; // 达到最大重连次数回调
// 初始化连接
this.connect();
}
// 建立连接
connect() {
// 关闭可能存在的旧连接
if (this.ws) {
this.ws.close(1000, "手动重连关闭旧连接");
this.ws = null;
}
// 创建新连接
try {
this.ws = new WebSocket(this.url);
this.isConnected = false;
// 监听连接成功
this.ws.onopen = (event) => {
this.isConnected = true;
this.reconnectAttempts = 0; // 重置重连次数
this.clearReconnectTimer(); // 清除重连定时器
this.onOpen(event); // 触发用户注册的 onOpen
};
// 监听消息
this.ws.onmessage = (event) => {
this.onMessage(event); // 触发用户注册的 onMessage
};
// 监听错误
this.ws.onerror = (error) => {
this.onError(error); // 触发用户注册的 onError
};
// 监听关闭(连接断开时触发重连)
this.ws.onclose = (event) => {
this.isConnected = false;
this.onClose(event.code, event.reason); // 触发用户注册的 onClose
// 不需要重连的情况(如手动关闭或服务器主动拒绝)
if (event.code === 1000 || event.code === 1001) {
console.log("WebSocket 正常关闭,不进行重连");
return;
}
// 触发重连
this.attemptReconnect();
};
} catch (error) {
this.onError(error);
this.attemptReconnect(); // 初始化失败时直接重连
}
}
// 尝试重连
attemptReconnect() {
// 检查是否达到最大重连次数
if (
this.options.maxReconnectAttempts !== -1 &&
this.reconnectAttempts >= this.options.maxReconnectAttempts
) {
this.onMaxReconnect();
console.error(
`已达到最大重连次数(${this.options.maxReconnectAttempts}次),停止重试`
);
return;
}
// 计算重连延迟(指数退避:延迟逐渐增加,避免频繁重试)
const delay = Math.min(
this.options.initialReconnectDelay *
Math.pow(this.options.reconnectDelayGrowth, this.reconnectAttempts),
this.options.maxReconnectDelay
);
this.reconnectAttempts++;
console.log(`第 ${this.reconnectAttempts} 次重连将在 ${delay}ms 后进行...`);
// 设置重连定时器
this.reconnectTimer = setTimeout(() => {
this.connect(); // 执行重连
}, delay);
}
// 发送消息(确保连接状态正常)
send(data) {
if (!this.isConnected || !this.ws) {
console.error("WebSocket 未连接,无法发送消息");
return false;
}
this.ws.send(data);
return true;
}
// 手动关闭连接(不会触发重连)
close(code = 1000, reason = "手动关闭") {
this.clearReconnectTimer();
if (this.ws) {
this.ws.close(code, reason);
}
this.isConnected = false;
}
// 清除重连定时器
clearReconnectTimer() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
}
// 使用示例
const ws = new ReconnectingWebSocket("ws://localhost:8080/ws");
// 注册事件回调
ws.onOpen = () => {
console.log("WebSocket 连接成功");
ws.send("客户端已连接"); // 连接成功后发送消息
};
ws.onMessage = (event) => {
console.log("收到消息:", event.data);
};
ws.onError = (error) => {
console.error("WebSocket 错误:", error);
};
ws.onClose = (code, reason) => {
console.log(`WebSocket 关闭(code: ${code}):${reason}`);
};
ws.onMaxReconnect = () => {
console.error("达到最大重连次数,请检查服务是否可用");
};
// 如需手动触发重连(可选)
// ws.connect();
// 如需手动关闭(可选)
// ws.close();// client
const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {};
ws.onmessage = (event) => {};
ws.onClose = () => {};
ws.onerror = (error) => {};
// 关闭连接
function closeWebSocket() {
if (socket.readyState === WebSocket.OPEN) {
// 可选参数:code(状态码)和 reason(原因描述)
socket.close(1000, "正常关闭"); // 1000 表示正常关闭的状态码
}
}
// node server
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
wss.on("connection", () => {
wss.on("message", () => {});
// 服务器主动关闭连接
function closeClientConnection() {
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, "服务器主动关闭");
}
}
wss.on("close", () => {});
});拖拽 API。
(被拖拽方)
| name | 时机 | 场景 |
|---|---|---|
| dragstart | 刚被拖拽时 | 记录拖拽元素 |
| drag | 持续 | 实时更新拖拽状态 |
| dragend | 结束时 | 清理样式 |
(接受拖拽)
| name | 时机 | 场景 |
|---|---|---|
| dragenter | 进入目标元素 | 目标高亮 |
| dragover | 在目标内持续移动 | 必须阻止默认行为(允许放置) |
| dragleave | 离开目标时 | 取消目标可放置状态 |
| drop | 拖拽在目标元素被释放时触发 | 执行放置逻辑 |
默认非拖拽 ,需设置draggable = 'true'
本地存储的区别。
| 区别 | localStorage | sessionStorage | indexedDB | cookie |
|---|---|---|---|---|
| 持久性 | 一直存在 | 页签关闭消失 | 永久 | 设置过期时间 |
| 存储大小 | 5m | 5m | 看磁盘空间 | 4kb |
| 作用域 | 同源下所有页面共享 | 当前页签/窗口共享 | 同源下所有页面共享 | |
| 数据类型 | string | string | 复杂对象二进制 | |
| 操作方式 | 同步 | 同步 | 异步 | |
| 适用场景 | 用户偏好 | 临时存储会话数据 | 存储大量结构化数据(离线应用缓存) | 用于身份认证(sessionID) |
主题切换
1.全局状态管理搭配系统设置,2.改变状态【dark/light】,3.改变document.documentElement.classList.add('dark'),4.从而影响 css 变量。
rem 适配移动端
1.根据设计稿与屏幕比列,得出根的 fontsize 也就是 rem。然后窗口改变重新计算。 2.通过postcss-pxtorem进行 px 转换。
浏览器渲染过程
每一帧的浏览器渲染过程的顺序为:

- 用户事件
- 一个宏任务
- 队列中全部微任务
- requestAnimationFrame
- 浏览器的重排/重绘
- requestIdleCallback
什么属性会触发重排,什么属性会触发重绘
重排
改变几何属性
- top
- height
- borderWidth
- padding
- margin
重绘
改变外观属性
- color
- bg
合成层
- transfrom
- opacity
什么是强制同步布局(forced Synchronous Layout)
正常流程:JS执行完 -> 浏览器统一做Layout
强制同步布局:你在JS里先修改了样式,然后立即读取几何属性(如offsetTop、clientHeight、getComputedStyle)浏览器被迫立即执行Layout,然后再继续执行JS。
const doms = document.querySelectorAll('.item')
// 每一轮循环都触发Layout 如果有1000个元素,就有1000次Layout。浏览器被逼疯了
for(let i =0;i<dom.length;i++) {
// 触发layout
const height = dom[i].offsetHeight;
// 标记layout
dom[i].style.height = height + 10 + 'px'
}Layout Thrashing(布局抖动)
当强制布局频繁发生时,页面会“抖动”,每一帧都在重新计算布局。性能急剧下降。
解决方案:读写分离。
const doms = document.querySelectorAll('.item')
const heights = [];
// 每一轮循环都触发Layout 如果有1000个元素,就有1000次Layout。浏览器被逼疯了
for(let i =0;i<dom.length;i++) {
// 读 触发一次layout
const height = dom[i].offsetHeight;
heights[i] = height;
}
for(let i =0;i<dom.length;i++) {
// 写
dom[i].style.height = heights[i];
}滚动卡顿解决方案
- performance录制,找到掉帧,看main线程 找出(layout/paint/js)
- layout频繁?,强制重新布局 -》 读写分离
- 检查是否频繁修改几何属性 -》改用transfrom
- 频繁修改颜色/背景 -》 合并处理或用css变量(跳过布局阶段)
- addEventListener('',fn, {passive:true} ) 不会阻止默认事件
- rAF
- 提升至合成层(慎用)如果js执行过长
- 拆分长任务 setTimeout webworker
- dom多,虚拟dom
let ticking = false;
window.addEventListener('scroll',(e)=>{
if(!ticking) {
requestAnimationFrame(()=>{
const scrollTop = window.pageYOffset;
header.style.opacity = scrollTop / 500;
ticking = false;
})
ticking = true;
}
})- 某些元素能不能再别的合成层,不参与layout
合成层
浏览器将页面分为多个合成层,每个合成层独立控制 。最终合成器将多个合成层合成为最终画面。如果一个图层的内容变了,只需要重新绘制这层,其他不同。
如何创建?
- transfrom
- opacity
- willChange:transfrom
- position:fixed
好处:动画只需要合成层处理,不触发layout和paint,性能极高。
滥用危害:每个合成层占用内存,滥用造成内存飙升,只在需要动画的元素上使用,动画结束后移除。
performance面板实战怎么定位问题
- 看FPS图表:红色长条代表掉帧
- 看火焰图(Main)线程:
- 红色长条 - layout(重排)
- 紫色长条 - 重绘
- 绿色长条 - js执行
- 黄色长条 - style计算
- 看layout shift:不稳定的元素移动
常见问题:
- 长黄色块(长任务)
- 紫色标记(强制同步布局)
LongTask
主线程执行时间超过 50ßms 的任务即为 Long Task
- 解决:基于 PerformanceObserver
// 1. 检查浏览器兼容性(现代浏览器均支持,IE不支持)
if (
window.PerformanceObserver &&
PerformanceObserver.supportedEntryTypes.includes("longtask")
) {
// 2. 创建Long Task监听器
const longTaskObserver = new PerformanceObserver((entryList) => {
// 3. 遍历所有捕获到的Long Task entry
entryList.getEntries().forEach((longTaskEntry) => {
// 4. 提取Long Task核心信息
const longTaskInfo = {
// 任务时长(必>50ms)
duration: longTaskEntry.duration.toFixed(2) + "ms",
// 任务开始时间(相对于页面导航)
startTime: longTaskEntry.startTime.toFixed(2) + "ms",
// 任务来源归因(关键:定位Long Task的产生原因)
sources: longTaskEntry.attribution.map((attr) => ({
type: attr.type, // 来源类型:如"script"(JS执行)、"layout"(布局计算)等
name: attr.name, // 来源名称:如脚本URL、DOM元素ID等
startTime: attr.startTime.toFixed(2) + "ms", // 来源任务的开始时间
})),
// 任务归属页面(区分主页面和iframe)
owner: longTaskEntry.name,
};
// 5. 处理Long Task信息(打印/上报)
console.warn("捕获Long Task:", longTaskInfo);
// 实际项目中:上报到服务端(如通过axios/postMessage)
// reportToServer('longtask', longTaskInfo);
});
});
// 6. 启动监听:指定监听"longtask"类型
longTaskObserver.observe({ entryTypes: ["longtask"] });
// (可选)页面卸载前停止监听,避免内存泄漏
window.addEventListener("beforeunload", () => {
longTaskObserver.disconnect();
});
} else {
console.warn("当前浏览器不支持Long Task监听");
}- 消除 longtask
- 场景 1 长时间 JS 执行
- 用 web workers 拆分计算密集型任务
- requestIdleCallback 处理非紧急任务
// 待处理的日志队列
const logQueue = [
/* 大量日志数据 */
];
// 空闲时处理日志(每次处理 10 条,避免单次耗时过长)
function processLogs(deadline) {
// deadline.timeRemaining():当前空闲时间(毫秒)
while (deadline.timeRemaining() > 0 && logQueue.length > 0) {
const log = logQueue.shift();
// 处理单条日志(如上报)
reportLog(log);
}
// 若队列未空,下一次空闲时继续处理
if (logQueue.length > 0) {
requestIdleCallback(processLogs);
}
}
// 启动空闲任务处理
requestIdleCallback(processLogs);- 拆分长循环
// 原始长循环(可能耗时 >50ms,产生 Long Task)
function badLoop() {
let result = [];
for (let i = 0; i < 10000; i++) {
result.push(heavyCalculation(i)); // 每次循环有耗时计算
}
}
// 优化:拆分循环,用 requestAnimationFrame 间隔执行
function optimizedLoop(total, batchSize = 100) {
let current = 0;
let result = [];
// 每次处理一个批次
function processBatch() {
const end = Math.min(current + batchSize, total);
for (; current < end; current++) {
result.push(heavyCalculation(current));
}
// 若未处理完,下帧继续
if (current < total) {
requestAnimationFrame(processBatch);
} else {
console.log("循环完成:", result);
}
}
// 启动批次处理
processBatch();
}
// 调用:处理 10000 次循环,每次批量 100 次
optimizedLoop(10000, 100);- 场景 2:强制同步布局/重绘 “强制同步布局” 指:先读取 DOM 布局属性(如 offsetWidth、getBoundingClientRect),再立即修改 DOM 样式,导致浏览器被迫重新计算布局(耗时),若频繁执行(如循环中),会产生 Long Task。
核心思路:先 “批量读取” 布局属性,再 “批量修改” 样式,避免读写交替。
Html-dom 的渲染过程
场景 3:大量 DOM 操作 核心思路:减少 DOM 操作次数,用 “离线 DOM” 或 “虚拟列表” 优化。
场景 4:第三方急哦啊笨 核心思路:让第三方脚本 “异步加载”,避免阻塞主线程。
输入 ip,dns 解析,http 三次握手建立链接,收到资源,浏览器解析 html 文档,经历布局,绘制,光栅化(将 dom 元素转化为位图)
1.解析 HTML 构建 DOM 树
过程:读取 HTML 字节 -> 转为字符 -> 令牌化 -> 构建节点 -> 形成 DOM 树
特点:遇到 script 会暂停解析执行 js,遇到 link、style 会并行下载 css
输出 树结构的 dom 2.解析 css 成 cssom 树
过程:解析外部 css、内连、行内;处理层叠规则和继承关系(如!important、选择器权重)
特点:css 解析树阻塞渲染的,从右到左解析 css 选择器(
.nav li a先解析 a)输出:cssom 树
3.合并 dom 和 cssom
过程遍历 DOM 树的每个可见节点
为每个节点找到匹配的 cssom 规则
组成形成包含所有可见节点以及样式的渲染树
4.布局/重排
过程:计算每个渲染树节点在屏幕的精确位置和尺寸;基于视口大小、盒模型】浮动和定位计算
触发条件:首次加载、窗口变化、元素位置/尺寸改变
关键指标:浏览器会尽量通过增量布局减少计算量
5.绘制/光栅化
过程:将布局结果转化为屏幕的实际像素;填充颜色、文本、图像、边框等视觉属性
层级处理:按层叠上下文顺序绘制,处理透明、混合模式等效果
优化技术:浏览器将元素提升至独立图层
6.合成:
过程:将不同图层合并成最终屏幕图像;应用 GPU 加速的变换
优势:避免重新布局和绘制,60fps 流畅动画的关键
关键性能优化点
1.减少重排
el.style.width = "100px";
el.style.height = el.offsetWidth + "px";
// 批量读写
requestAnimationFrame(() => {
el.style.width = "100px";
el.style.height = el.offsetWidth + "px";
});2.选择器优化性能
避免嵌套过深
优先使用类选择器而非属性选择器 ps:为什么? 类选择器可以快速缩小范围,维护了类名的索引映射;属性选择器需要遍历检查每一个元素的属性
3.图层管理
.animate-element {
will-change: transform;
transform: translateZ(0);
}渲染阻塞行为
| 资源类型 | 解析阻塞 | 渲染阻塞 | 解决方案 |
|---|---|---|---|
| script | 1 | 1 | async/defer |
| link css | 0 | 1 | 媒体查询 media=print/仅内嵌 |
| 图片/字体 | 0 | 1 | 预加载` |
link 标签的作用
加载 css
<link rel="stylesheet" href="style.css">
- 媒体查询:
media="print"指定特定媒体类型
- 媒体查询:
- 完整性校验
integrity属性 + corssorigin=anonymous
- 完整性校验
替代样式表:
rel="alternate stylesheet"网站图标与资源标识
<link rel='icon' href='/icon.svg'>
rel="apple-touch-icon"
rel="manifest"
rel='mask-icon'
预加载、dns 解析、预链接
rel='preload'
rel='dns-prefetch'
rel='preconnect'
rel-'prefetch'
替代内容与备份资源
- 替换样式表
rel="alternate stylesheet"
- 替换样式表
- RSS/Atom 订阅
rel="alternate" type="application/rss+xml"
- RSS/Atom 订阅
- 多语言版本
rel='alternate' hreflang="en"
- 多语言版本
资源加载优先级
浏览器根据 rel 属性确定资源加载优先级
| rel 值 | 资源类型 | 优先级 | 说明 |
|---|---|---|---|
| preload | 关键资源 | 最高 | 立即加载,阻塞渲染 |
| sheet | css | 最高 | 阻塞渲染,影响首次渲染 |
| preconnect | 连接 | 中高 | 提前建立连接,抢先执行部分或全部握手 |
| dns-prefetch | DNS | 低 | 后台 dns 预解析 |
| prefetch | 非关键资源 | 最低 | 空闲时加载,用与未来页面 |
最佳实践
| 实践 | 具体 |
|---|---|
| 关键 Css 内联 | 首屏关键放在 html 里 |
| 非关键 css 异步加载 | media='print' onload='this.media=all' |
| 预加载关键资源 | 字体、首屏图片、关键脚本 |
| 使用 dns-preftech/preconnect | 特别是第三方资源 |
| SRI 完整性校验 | 确保 cdn 资源未被篡改 |
| 提供现代图标格式 | 优先使用 svg 格式图标 |
Shadow DOM
- 概念:浏览器元素的 DOM 封装技术
- 作用:通过样式、脚本隔离实现组件封装,是 web components 标准的重要组成部分。
- 优点:支持良好,广泛用于原生组件和 UI 库
- 劣势:受限于学习成本和框架竞争,流行程度不及 React/Vue 等框架的组件方案。
- 场景:追求 无框架依赖 或 原生组件化的场景中发挥优势。
- 举例
<body>
<my-component>
<p slot="main-content" class="slotted-text">
这段文本从主DOM插入到Shadow插槽
</p>
</my-component>
</body>
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `<style>.red{color:red} .green {color:green}</style>
<div class='red'>
<p class=‘text’ part="shadow-text"> 这是shadow dom里的p</p>
red text
<!-- 定义插槽,接收主 DOM 插入的内容 -->
<slot name="main-content"></slot>
</div>
`;
}
connectedCallback() {
this.shadowRoot.querySelector("div").classList.add("green");
}
}
customElements.define("my-component", MyComponent);
</script>
<style>
.my-component::part(shadow-text) {
color: yellow;
}
/* 给主DOM插入到Slot的元素内容添加样式 */
.my-component ::slotted(.slotted-text) {
color: blue;
}
</style>对比 xhr 和fetch
| 特性 | XMLHttpRequest | Fetch API | 胜出方 |
|---|---|---|---|
| 语法简简洁 | 冗长 | 简洁 | Fetch |
| Promise支持 | 需封装 | 原生 | Fetch |
| 错误处理 | 全面 | 需手动处理 | XHR |
| 超时控制 | 原始支持 | 需自己实现 | XHR |
| 请求取消 | xhr.abort() | abortControl.abort | 平手 |
| 进度监控 | 完整事件 | 有限支持 | XHR |
| 流式处理 | 不支持 | 支持 | Fetch |
| cors | 完整 | 完整 | 平手 |
| 浏览器支持 | 全平台 | ie不支持 | XHR |
| 缓存控制 | 完整 | 完整 | 平手 |
| 请求拦截 | 可重写 | 可包装 | 平手 |
fetch
- Fetch与Streams API结合更强大
- fetch是service worker的核心api
- fetch更好支持http2特性
- 现在框架 React query、SWR等库内部使用Fetch
// fetch 封装通用库
// 1.请求配置,2.超时取消控制,3.返回类型控制
class HttpClient {
constructor(baseURL = '', options = {}) {
this.baseURL = baseURL;
this.defaultOptions = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
credentials: 'same-origin',
...options
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...this.defaultOptions,
...options,
headers: {
...this.defaultOptions.headers,
...options.headers
}
};
// 超时控制
const { timeout = 10000 } = config;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...config,
signal: controller.signal
});
clearTimeout(timeoutId);
// 处理响应
if (!response.ok) {
throw new HttpError(response.status, response.statusText);
}
// 根据Content-Type解析响应
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时');
}
throw error;
}
}
get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data),
credentials: 'same-origin' // 发送cookie: 'same-origin' | 'include' | 'omit'
});
}
// 其他方法...
}
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}// fetch如何取消请求
const ab = new AbortController()
fetch('/app/getdata',{signal:ab.signal}).then(res=>res.json()).catch(err=>{
if(error.name === "AbortError") {
console.log('请求被被取消')
}
})
ab.abort()ArrayBuffer
1.什么是buffer?
buffer是node特有的,用于处理二进制数据的类,在浏览器环境中,用ArrayBuffer替代
// node
const buf = Buffer.from("hello",'utf8')
console.log(buf) // <Buffer 48 65 6c 6c 6f>
console.log(buf.toString('hex')) // 48656c6c6f
console.log(buf.toString('base64')) // SGVsbG8=
// Buffer实际是Unit8Array的子类2.buffer和ArrayBuffer的关系
// Buffer -> ArrayBuffer
const buffer = Buffer.from('hello')
const arraybuffer = buffer.buffer; //底层的ArrayBuffer
// ArrayBuffer -> Buffer
const arrayBuffer = new ArrayBuffer(10)
const bufferFromArr = Buffer.from(arrayBuffer)3.blob是什么
Blob(Binary Large Object)不可变的原始数据,常用文件操作
//. new Blob([JSON.stringify({a:2}) ],{type:"application/json"});
//. new Blob([],{ });
const blob = new Blob(["Hello word"],{type:"text/plain"})
async function readBlob(blob) {
const test = await blob.text()
const arrayBuffer = await blob.arrayBuffer()
const dataUrl = await new Promise((resolve)=>{
const reader = new FileReader();
reader.onload = ()=>resolve(reader.result);
reader.readAsDataUrl(blob);
})
}4.Unit8Array时什么
TypedArray是专门用于处理二进制数据的类型化数组,Unit8Array是其中的一种
// 各种TypedArray
const typedArrays = {
Int8Array: '8位有符号整数',
Uint8Array: '8位无符号整数(0-255)', // 最常用
Uint8ClampedArray: '8位无符号整数(限制在0-255)',
Int16Array: '16位有符号整数',
Uint16Array: '16位无符号整数',
Int32Array: '32位有符号整数',
Uint32Array: '32位无符号整数',
Float32Array: '32位浮点数',
Float64Array: '64位浮点数'
};// 常见操作
const unit8 = new Unit8Array(5)
uint8[0] = 72; // H
uint8[1] = 101; // e
uint8[2] = 108; // l
uint8[3] = 108; // l
uint8[4] = 111; // o
// 从字符串创建
const encoder = new TextEncoder();
const uint8FromText = encoder.encode('Hello');
// 转换为字符串
const decoder = new TextDecoder();
const text = decoder.decode(uint8FromText); // "Hello"
// 与普通数组的转换
const normalArray = Array.from(uint8); // [72, 101, 108, 108, 111]
const fromArray = Uint8Array.from([72, 101, 108, 108, 111]);5.base64是什么
Base64是一种用于64个字符表示二进制数据的编码方式
// Base64 字符集
const base64Chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789+/';
// Base64 编码原理:3字节 → 4个Base64字符// 转换方式
const text = 'hello'
const base64 = btoa(text) // "SGVsbG8sIFdvcmxkIQ=="
let otext = atob(base64) // "Hello, World!"
// 注意:btoa/atob 只能处理Latin1字符
// 处理UTF-8需要转义
function encodeUTF8Base64(str) {
return btoa(unescape(encodeURIComponent(str)));
}
function decodeUTF8Base64(base64) {
return decodeURIComponent(escape(atob(base64)));
}//转换
// arrayBuffer -> unit8Array
// unit8Array -> arrayBuffer
// unit8Array -> base64
function unit8ArrayToBase64(unit8Arrayl) {
let buf = '';
for(let i =0;i<unit8Arrayl.length;i++) {
buf += String.fromCharCode(unit8Arrayl[i])
}
return btoa(buf)
}
// base64 -> unit8Array
function base64ToUnit8Array(base64) {
let str = atob(base64);
let buf = new Unit8Array(str.length);
for(let i=0;i<str.length;i++) {
buf[i] = str.charCodeAt(i);
}
return buf;
}
function BlobToBase64(blob) {
return new Promise((resolve,reject)=>{
const r = new FileReader();
r.onloaded = ()=>{
resolve(r.result)
}
r.readAsDataUrl(blob);
})
}
function base64ToBlob(base64,contentType,splitSize = 512){
let buf = atob(base64)
let len = [];
for(let i =0; i<buf.length;i+=splitSize) {
let temp = buf.slice(i,i+splitSize);
for(let j=i+0;j<i+temp.length;j++) {
len[j]= temp.charCodeAt(j-i)
}
}
return new Blob([new Unit8Array(len)],{type:contentType})
}原始数据
↓
ArrayBuffer (原始二进制)
├── Uint8Array (视图)
├── Blob (文件对象)
└── 其他TypedArray
↓
Base64 (文本表示)ASCII与二进制的关系详解
1.ASCII是什么?
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是一种字符编码标准,用于将字符(字母、数字、符号)映射为数字。
2.范围
const asciiRange = {
controlCharacters: 0-31, // 控制字符
printableCharacters: 32-126, // 可打印字符
deleteCharacter: 127, // 删除字符
total: 128 // 2⁷ = 128
};3.转化流程
A -> 65 -> 01000001 -> 计算机存储4.二进制与ascii与base64
原始数据 → 二进制表示 → ASCII 字符表示 → Base64 编码EncodeURIComponent
一句话总结:把url上的中文、空格、字符(/ & ? = # @ + : , ;)变成浏览器能安全传输的URL编码格式。
URL对象
是 内存对象 和 浏览器加载器 之间的连接桥梁,生成一个指向内存对象的临时URL,需要手动释放。
| 特性 | 说明 |
|---|---|
| 指向原对象 | URL直接引用内存中的Blob,不复制数据 |
| 同源策略 | 生成的URL属于当前页源,仅当前页可用 |
| 临时性 | 关闭页面或手动撤销后失效 |
| 协议 | 使用blob:协议 非http: 或file: |
为啥不用base64?
| 场景 | URL.createObjectURL | base64 Data URLs |
|---|---|---|
| 大文件 | 低内存 | 高内存(字符串复制) |
| 图片视频渲染 | 快(直接解码) | 慢(解+33%膨胀) |
| 重复使用 | 同引用 | 每次生成新的 |
| 内存管理 | 手动撤销 | 随变量释放 |
let png = new Blob([data],{type:'img/png'})
const objUrl = URL.createObjectURL(png)
// eg.2
videoInput.addEventListener('change',(e)=>{
let f = e.target.files[0]
let url = URL.createObjectURL(f);
let dom = docuent.getElementById('player')
dom.url = url;
dom.onLoad =()=> URL.revokeObjectURL(url)
})