Skip to content

html

html5 的新特性

标签含义与使用场景
header页面或者区域头部,通常包括标题,logo,导航栏
footer页面或者区域的底部,通常包括版权、联系方式、相关链接
nav导航栏区域,用于包裹住要的导航链接(主导航、侧边栏导航—)
article独立完整的“文章类”内容(博客、新闻、评论)可单独存在并被理解
section页面中的“区块”(用于对内容分组),通常包含一个标题(h1-h6)强调“主题性”
aside侧边栏或“辅助内容”(如文章的相关推荐、作者信息、广告),与主内容相关但非必需

媒体标签

标签含义与使用场景
videosrc/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>

为什么用语义化标签

  1. 代码可读性、SEO 友好(搜索引擎更易理解内容结构),便于屏幕阅读器(无障碍访问),减少冗余 class 命名

SEO,需要知道的细节

优化思路

  1. 页面结构优化
html
<!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>
  1. 内容优化 保证页面中关键词的覆盖率

  2. 技术向SEO优化

  • 地图
  • 结构化数据
  • 移动端兼容
css
@media (max-width : 600px) {
  body{

  }
}

history,vue 路由的 hash 模式和 history 有什么区别?动态路由?按需加载?鉴权?

history 是 h5 提供的历史记录管理 api,允许 javascript 操作浏览器的会话历史,实现不刷新页面的情况下修改 URL,添加/替换历史记录功能。 hash 是页面的锚点

区别hashhistory
URL 格式#/
是否需要服务端支持不需要需要(访问不存在页面,会报 404)
底层原理监听 hash change基于 h5 的 pushState
兼容性全部兼容依赖 h5 ie>10+
SEO 友好性较差较好(URL 更规范)
apixx-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 的区别

对比维度httpwebsocket
连接方式短连接长连接
方向
头部开销每次请求携带完整头部握手后无冗余头部,开销低
场景网页请求聊天、实时传输
协议表示https://wss://

websocket 和 SSE 的区别

维度WebSocketSSE
方向全双工单项
协议基础独立协议(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流式输出
ts
// 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);
  });
});
ts
// 检查点机制
// 客户端实现检查点
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);
  }
}
ts
// 会话状态恢复(AI应用专用)

应用

解决socket断联问题

  • 心跳检测。客户端或者服务端主动发送一个空包,对端同样返回相同空包,保障连接的稳定性和持久性。
    • 流程:每30s发送心跳包heartbeat,后端立即响应,前端15s内未收到,则标记为异常。后端每30s检测客户端最后存活时间,若60s内未收到心跳,则主动断开连接。
  • 自动重连。客户端监测到断开,可采用指数退避的方式实现自动重连。

重连设计:

  • 自动重连触发:报错重连attemptReconnect
  • 重连策略:重连次数限制,重连时间递增1s 每次乘1.5s 最大不超过10s
  • 连接状态管理:标记isConnected,重连前清空旧资源
  • 提供事件回调
js
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();
js
// 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'

本地存储的区别。

区别localStoragesessionStorageindexedDBcookie
持久性一直存在页签关闭消失永久设置过期时间
存储大小5m5m看磁盘空间4kb
作用域同源下所有页面共享当前页签/窗口共享同源下所有页面共享
数据类型stringstring复杂对象二进制
操作方式同步同步异步
适用场景用户偏好临时存储会话数据存储大量结构化数据(离线应用缓存)用于身份认证(sessionID)

主题切换

1.全局状态管理搭配系统设置,2.改变状态【dark/light】,3.改变document.documentElement.classList.add('dark'),4.从而影响 css 变量。

rem 适配移动端

1.根据设计稿与屏幕比列,得出根的 fontsize 也就是 rem。然后窗口改变重新计算。 2.通过postcss-pxtorem进行 px 转换。

浏览器渲染过程

每一帧的浏览器渲染过程的顺序为:

alt text

  1. 用户事件
  2. 一个宏任务
  3. 队列中全部微任务
  4. requestAnimationFrame
  5. 浏览器的重排/重绘
  6. requestIdleCallback

什么属性会触发重排,什么属性会触发重绘

重排

改变几何属性

  • top
  • height
  • borderWidth
  • padding
  • margin

重绘

改变外观属性

  • color
  • bg

合成层

  • transfrom
  • opacity

什么是强制同步布局(forced Synchronous Layout)

正常流程:JS执行完 -> 浏览器统一做Layout

强制同步布局:你在JS里先修改了样式,然后立即读取几何属性(如offsetTop、clientHeight、getComputedStyle)浏览器被迫立即执行Layout,然后再继续执行JS。

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(布局抖动)

当强制布局频繁发生时,页面会“抖动”,每一帧都在重新计算布局。性能急剧下降。

解决方案:读写分离。

js
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
js
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
js
// 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 执行
  1. 用 web workers 拆分计算密集型任务
  2. requestIdleCallback 处理非紧急任务
js
// 待处理的日志队列
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);
  1. 拆分长循环
js
// 原始长循环(可能耗时 >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.减少重排

js
el.style.width = "100px";
el.style.height = el.offsetWidth + "px";

// 批量读写
requestAnimationFrame(() => {
  el.style.width = "100px";
  el.style.height = el.offsetWidth + "px";
});

2.选择器优化性能

  • 避免嵌套过深

  • 优先使用类选择器而非属性选择器 ps:为什么? 类选择器可以快速缩小范围,维护了类名的索引映射;属性选择器需要遍历检查每一个元素的属性

    3.图层管理

css
.animate-element {
  will-change: transform;
  transform: translateZ(0);
}

渲染阻塞行为

资源类型解析阻塞渲染阻塞解决方案
script11async/defer
link css01媒体查询 media=print/仅内嵌
图片/字体01预加载`
  • 加载 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"
    • 多语言版本 rel='alternate' hreflang="en"

资源加载优先级

浏览器根据 rel 属性确定资源加载优先级

rel 值资源类型优先级说明
preload关键资源最高立即加载,阻塞渲染
sheetcss最高阻塞渲染,影响首次渲染
preconnect连接中高提前建立连接,抢先执行部分或全部握手
dns-prefetchDNS后台 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 等框架的组件方案。
  • 场景:追求 无框架依赖 或 原生组件化的场景中发挥优势。
  • 举例
html
<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

特性XMLHttpRequestFetch 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
js
// 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;
  }
}
js
// 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替代

js
// 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的关系

js
// 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)不可变的原始数据,常用文件操作

js
//. 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是其中的一种

js
// 各种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位浮点数'
};
js
// 常见操作
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个字符表示二进制数据的编码方式

js
// Base64 字符集
const base64Chars = 
  'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
  'abcdefghijklmnopqrstuvwxyz' +
  '0123456789+/';
// Base64 编码原理:3字节 → 4个Base64字符
js
// 转换方式
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)));
}
js
//转换

// 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.范围

js
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.createObjectURLbase64 Data URLs
大文件低内存高内存(字符串复制)
图片视频渲染快(直接解码)慢(解+33%膨胀)
重复使用同引用每次生成新的
内存管理手动撤销随变量释放
js
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)
})

Released under the MIT License.