前端面试题总结

292 阅读9分钟

笔试题一

1. div+css 的布局较 table 布局有什么优点?

答案解析:

div+CSS 是结构与样式分离的布局方案,而 table 布局本质是为表格数据设计的,二者在布局场景下的核心差异如下,div+CSS 的优点主要体现在: image.png

2. cookies、sessionStorage 和 localStorage 的区别:

答案解析:

特性cookiessessionStoragelocalStorage
存储大小较小(约 4KB),仅适合存少量数据中等(约 5MB)中等(约 5MB)
有效期可设置(通过 expires/max-age),过期后自动删除;未设置则为「会话级」(关闭浏览器删除)会话级(关闭标签页 / 浏览器即删除,不受窗口刷新影响)永久级(除非手动删除或代码清除,关闭浏览器后仍保留)
存储位置浏览器端(内存 / 磁盘,依有效期而定)+ 随 HTTP 请求发送到服务器仅浏览器内存(不与服务器交互)仅浏览器磁盘(不与服务器交互)
是否随请求发送是(每次 HTTP 请求的 Header 中携带,需注意体积)否(纯客户端存储,不参与网络请求)否(纯客户端存储,不参与网络请求)
作用域基于域名(子域名可访问父域名 cookies,需配置 domain基于标签页(同一浏览器不同标签页不共享,即使同域名)基于域名(同一域名下所有标签页 / 窗口共享)
API 便捷性原生 API 繁琐(需手动处理编码 / 解码),需封装原生 API 简洁(setItem/getItem/removeItem原生 API 简洁(同 sessionStorage)
典型用途存储登录状态(如 token)、用户身份标识(sessionId存储临时表单数据(如未提交的表单内容)、临时状态(如步骤条进度)存储用户偏好(如主题设置、语言选择)、长期缓存数据(如离线应用资源)

3. 在 css/js 代码上线之后开发人员经常会优化性能,从用户刷新网页开始,一次 js 请 求一般情况下有哪些地方会有缓存处理?

答案解析:

用户刷新网页后,JS 请求的缓存会经历「浏览器本地缓存 → 网络缓存 → 服务器缓存」的多层级处理,目的是减少重复请求、提升加载速度,具体阶段如下:

  1. 内存缓存(Memory Cache) - 优先级最高,浏览器将刚加载过的 JS(如当前会话内的请求)暂存到内存中,刷新页面时直接从内存读取,无需磁盘 I/O 和网络请求。 - 特点:速度最快,但内存有限,关闭标签页后缓存失效(内存释放)。
  2. 磁盘缓存(Disk Cache,即 HTTP 缓存)
    若内存缓存未命中,浏览器会检查磁盘中的 HTTP 缓存,这是最核心的缓存层,分为「强缓存」和「协商缓存」: - 强缓存:通过响应头 Cache-Control(如 max-age=3600)或 Expires 控制,若缓存未过期,直接从磁盘读取,不发送 HTTP 请求。 - 协商缓存:若强缓存过期,浏览器发送 HTTP 请求,携带 If-None-Match(对应服务器 ETag,文件内容哈希)或 If-Modified-Since(对应服务器 Last-Modified,文件修改时间),服务器判断文件是否变化: - 未变化:返回 304 Not Modified,浏览器使用磁盘缓存; - 已变化:返回 200 OK 和新文件,更新磁盘缓存。
  3. CDN 缓存
    若 HTTP 缓存未命中(如首次请求或文件更新),请求会到达 CDN(内容分发网络): - CDN 节点会缓存常用 JS 文件,若节点已缓存且未过期,直接返回文件; - 若 CDN 未缓存或已过期,CDN 会向源服务器请求文件,缓存后再返回给用户(下次同区域用户可直接从 CDN 读取)。
  4. 服务器端缓存
    若 CDN 未命中,请求最终到达源服务器: - 服务器会检查本地缓存(如 Redis、Memcached),若缓存了该 JS 文件(如打包后的静态资源),直接返回; - 若服务器缓存未命中,才会读取磁盘中的 JS 文件,返回给客户端(同时更新服务器缓存和 CDN 缓存)。

4. 什么是外边距重叠?重叠的结果是什么?

答案解析:

定义:相邻的块级元素(或同一元素内的特定情况)的垂直外边距(margin-top/margin-bottom)  会合并为一个单一的外边距,称为「外边距重叠」。水平外边距不会重叠

重叠的常见场景及结果:重叠后的外边距值遵循「取大原则」(若存在负外边距,则为「叠加原则」):

场景 1:相邻兄弟元素
两个相邻的块级兄弟元素,上方元素的 margin-bottom 和下方元素的 margin-top 会重叠,结果为「两者中的较大值」。
例: .box1 { margin-bottom: 20px; } .box2 { margin-top: 10px; } 最终两元素间距为 20px(取较大值),而非 30px

场景 2:父元素与第一个 / 最后一个子元素
父元素没有 paddingborderoverflow: hidden 等隔离时,父元素的 margin-top 会与第一个子元素的 margin-top 重叠;父元素的 margin-bottom 会与最后一个子元素的 margin-bottom 重叠,结果为「两者中的较大值」。
例: .parent { margin-top: 10px; } .child { margin-top: 20px; }
最终父元素整体的 margin-top 为 20px(子元素的 margin 「穿透」父元素)。

场景 3:空块级元素
一个无内容、无 paddingborder 的空块级元素,其 margin-top 和 margin-bottom 会重叠,结果为「两者中的较大值」(若均为正值,取大;若有负值,叠加)。
例: .empty-box { margin-top: 10px; margin-bottom: 15px; } 最终元素的垂直间距为 15px

5. 有这样一个 URL: item.taobao.com/item.htm?a=…, 请写一 段 JS 程序提取 URL 中的各个 GET 参数 (参数名和参数个数不确定), 将其按 key-value 形式返回到一个 json 结构中,如 {a:'1', b:'2', c:'', d:'xxx', e:undefined}。

答案解析:

需求分析

需处理以下边界情况:

  • URL 无 ?(无参数)→ 返回空对象;
  • 参数无值(如 c=)→ 值为 ''
  • 参数无 =(如 e)→ 值为 undefined
  • 参数值含特殊字符(如 &,但 URL 中会编码为 %26,需解码)。

代码实现

function getUrlParams(url) {
const params = {};
// 1. 分割 URL,获取 query 部分(?后面的内容)
const queryStr = url.split('?')[1];
if (!queryStr) return params; // 无参数,返回空对象
// 2. 分割 query 为单个键值对(&分隔)
const queryArr = queryStr.split('&');
// 3. 遍历处理每个键值对
queryArr.forEach(item => {
// 分割键和值(=分隔,最多分割1次,避免值中含=的情况)
const [key, value = undefined] = item.split('=');
// 解码 URL 编码(如 %26 解码为 &)
const decodeKey = decodeURIComponent(key);
const decodeValue = value !== undefined ? decodeURIComponent(value) : undefined;
params[decodeKey] = decodeValue;
});
return params;
}
// 测试:传入题目 URL
const url = 'http://item.taobao.com/item.htm?a=1&b=2&c=&d=xxx&e';
console.log(getUrlParams(url)); 
// 输出:{ a: '1', b: '2', c: '', d: 'xxx', e: undefined }

6. iframe 的优缺点

答案解析:

优点

  1. 上下文隔离:iframe 内的 HTML/CSS/JS 与父页面完全隔离,不会相互污染(如父页面的样式不会影响 iframe 内的元素,反之亦然),适合嵌入第三方内容(如广告、地图、支付页面)
  2. 异步加载不阻塞:iframe 内的资源加载不会阻塞父页面的解析和渲染(除非设置 async=false),可优化首屏加载速度。
  3. 历史记录管理:通过 window.frames 或 postMessage 可独立管理 iframe 内的历史记录(如前进 / 后退)。
  4. 复用独立功能:将复杂功能(如富文本编辑器、视频播放器)封装为 iframe,可在多个页面复用,降低耦合。

缺点

  1. 性能开销大

  • 每个 iframe 会触发独立的 HTTP 请求和渲染流程,增加浏览器负担;

  • iframe 会阻塞父页面的 load 事件(需等待 iframe 加载完成),可通过 loading="lazy" 优化。

  1. SEO 不友好:搜索引擎难以抓取 iframe 内的内容,若核心内容在 iframe 中,会影响页面排名。
  2. 跨域通信复杂:父页面与 iframe 间的通信需通过 postMessage(需处理跨域权限),无法直接访问对方的 window 对象。
  3. 样式控制难:iframe 的高度自适应(随内容变化)需通过 JS 监听内容高度,原生 CSS 无法实现;部分浏览器对 iframe 的边框、滚动条样式控制有限。

7. JS 中的 call () 和 apply () 方法的区别

答案解析:

call() 和 apply() 均为函数的原型方法,核心作用是改变函数执行时的 this 指向,并立即执行函数,二者的唯一区别是参数传递方式

语法对比

方法语法参数说明
call()function.call(thisArg, arg1, arg2, ...)thisArg:函数执行时的 this 指向(若为 null/undefined,非严格模式下指向 window); - arg1, arg2,...:函数的参数,需逐个传入
apply()function.apply(thisArg, [arg1, arg2, ...])thisArg:同 call(); - 第二个参数:函数的参数,需以数组或类数组(如 arguments  形式传入。

示例

const obj = { name: '张三' };
function sayHi(age, gender) {
  console.log(`我是${this.name},年龄${age},性别${gender}`);
}

// call():逐个传参
sayHi.call(obj, 20, '男'); // 输出:我是张三,年龄20,性别男

// apply():数组传参
sayHi.apply(obj, [20, '男']); // 输出:我是张三,年龄20,性别男

// 特殊场景:求数组最大值(Math.max 不支持数组参数)
const arr = [1, 3, 2];
console.log(Math.max.call(null, ...arr)); // 3(ES6 扩展运算符,等价于 call(null,1,3,2))
console.log(Math.max.apply(null, arr));  // 3(直接传数组)

相同点

  • 均立即执行函数(区别于 bind()bind() 仅绑定 this 不执行函数);
  • 若 thisArg 为基本类型(如 number、string),会自动转为对应的包装对象(如 1 → Number {1})。

8. 用正则表达式,写出由字母开头,其余由数字、字母、下划线组成 的 6~30 的字符串

答案解析:

const reg = /^[a-zA-Z][a-zA-Z0-9_]{5,29}$/;

  • ^:匹配字符串开头,确保首字符符合规则;
  • [a-zA-Z]:匹配首字符(大小写字母);
  • [a-zA-Z0-9_]:匹配后续字符(字母、数字、下划线);
  • {5,29}:限制后续字符的长度为 5-29 位(总长度 = 1 + 5-29 = 6~30);
  • $:匹配字符串结尾,确保没有多余字符(如特殊符号)。

9. 获取计算机上连接的摄像头的方法,以及有哪些需要注意的地方。

答案解析:

核心方法:navigator.mediaDevices.getUserMedia()

该 API 是浏览器提供的「媒体捕获 API」,用于获取设备的摄像头 / 麦克风权限,返回一个 Promise,成功后获取 MediaStream(媒体流),可绑定到 video 元素播放。

代码示例

<!-- HTML:用于展示摄像头画面 -->
<video id="video" autoplay playsinline></video>

<script>
async function getCamera() {
 const videoElem = document.getElementById('video');
 try {
   // 1. 检查 API 支持(旧浏览器需兼容前缀,如 webkitGetUserMedia)
   if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
     throw new Error('浏览器不支持摄像头访问');
   }

   // 2. 请求摄像头权限(参数 { video: true } 表示只获取视频,不获取音频)
   // 可选配置:指定分辨率 { video: { width: 1280, height: 720 } }
   const stream = await navigator.mediaDevices.getUserMedia({ video: true });

   // 3. 将媒体流绑定到 video 元素,展示摄像头画面
   videoElem.srcObject = stream;

   // 4. (可选)停止摄像头(如关闭页面时释放资源)
   // stream.getTracks().forEach(track => track.stop());
 } catch (err) {
   // 处理错误(如用户拒绝权限、无摄像头设备)
   console.error('获取摄像头失败:', err.message);
   alert('请允许摄像头权限,或检查设备是否连接摄像头');
 }
}

// 调用函数
getCamera();
</script>

注意事项

  • 必须使用 HTTPS:除 localhost(本地开发环境)外,浏览器要求必须通过 HTTPS 协议访问(否则会阻止摄像头请求,提示「不安全的上下文」)。

  • 用户授权:API 会触发浏览器的权限弹窗,用户必须点击「允许」才能获取摄像头,若用户拒绝,需引导其在浏览器设置中重新开启权限。

  • 设备兼容性

    • 旧浏览器(如 IE)不支持该 API,需兼容时可使用 navigator.webkitGetUserMedia(Chrome/Safari 旧版本);
    • 部分设备(如无摄像头的电脑)会返回错误,需捕获并提示用户。
  • 资源释放:使用完摄像头后,必须调用 stream.getTracks().forEach(track => track.stop()) 释放资源,避免占用设备内存。

  • 隐私安全:开发者需明确告知用户摄像头的使用目的(如「用于视频通话」),遵守隐私法规(如 GDPR),不得未经允许录制用户画面。

10. 双向绑定原理

答案解析:

Vue 的双向绑定(v-model)本质是「数据驱动视图 + 视图反馈数据」的闭环,核心依赖「数据劫持」和「依赖收集」,Vue 2 和 Vue 3 的实现方式有差异。

Vue 2 实现:Object.defineProperty()

核心步骤:

  1. 数据劫持(Observer)
    Vue 初始化时,对 data 中的每个属性用 Object.defineProperty() 重写 getter 和 setter

  • getter:当属性被访问时(如模板渲染、JS 中读取),触发「依赖收集」;

  • setter:当属性被修改时(如 this.msg = 'new'),触发「视图更新」。

  1. 依赖收集(Dep + Watcher)

  • Dep(依赖容器) :每个被劫持的属性对应一个 Dep,用于存储依赖该属性的 Watcher

  • Watcher(观察者) :模板编译时,每个绑定数据的 DOM 节点(如 {{ msg }})会生成一个 Watcher,访问属性时触发 getter,将 Watcher 加入 Dep

  1. 视图更新
    当属性被修改时,触发 setterDep 会通知所有关联的 WatcherWatcher 调用「更新函数」,通过 Virtual DOM 的 diff 算法找到最小变更,更新真实 DOM。
Vue 3 实现:Proxy

Vue 2 的 Object.defineProperty() 存在缺陷(无法监听对象新增 / 删除属性、数组索引修改),Vue 3 改用 Proxy 实现数据劫持,优势如下:

  1. 监听整个对象:无需递归劫持每个属性(Proxy 直接代理整个 data 对象),性能更优;
  2. 支持更多场景:可监听对象新增 / 删除属性(如 this.obj.newKey = 'val')、数组索引修改(如 this.arr[0] = 'new')、Array.prototype 方法(如 push/pop);
  3. 返回代理对象Proxy 返回一个新的代理对象,而非修改原对象,逻辑更清晰。
v-model 语法糖

v-model 是双向绑定的语法糖,本质是「绑定 value 属性 + 监听 input 事件」,例:

<!-- 等价于 <input :value="msg" @input="msg = $event.target.value" /> --> 
<input v-model="msg" />

11. Vue 间组件参数传递(父子,兄弟)

答案解析:

父子组件

  • 父传子:通过props传递数据,子组件在props选项中声明接收的属性。
  • 子传父:子组件通过$emit触发自定义事件,父组件通过v-on监听事件并获取数据。

兄弟组件

  • 方式 1:通过父组件中转(子 A 传父,父再传子 B);
  • 方式 2:使用事件总线(EventBus) (创建一个空 Vue 实例,通过$on$emit实现跨组件通信);
  • 方式 3:使用状态管理库(如 Vuex、Pinia),将共享数据集中管理,兄弟组件通过 “读 / 写状态” 实现通信。

12. Virtual DOM 与真实 DOM 的理解

答案解析:

真实 DOM(Real DOM) :浏览器渲染引擎中的「文档对象模型」,是对 HTML 文档的树形结构抽象,每个节点都是一个 HTMLElement 对象,操作真实 DOM 会触发「重排(Reflow)」和「重绘(Repaint)」,性能开销大。

Virtual DOM(虚拟 DOM) :用 JavaScript 对象 模拟的真实 DOM 结构,本质是「轻量级的 DOM 描述」

对比维度真实 DOMVirtual DOM
本质浏览器渲染引擎的原生对象JS 对象(模拟 DOM 结构) >
操作性能操作开销大(修改会触发重排 / 重绘)操作开销小(仅修改 JS 对象,无浏览器渲染)
跨平台能力仅能在浏览器中使用可跨平台(如 Vue SSR 渲染为 HTML,Weex 渲染为原生组件)
内存占用占用内存大(含大量浏览器内置属性)占用内存小(仅保留核心属性)

13. 创建一个立方体,有 6 张图片,分别为 negx.jpg、negy.jpg、negz.jpg、posx.jpg、posy.jpg、posz.jpg。请给立方体贴图

答案解析: Threejs 中为立方体贴图需使用 CubeTextureLoader 加载 6 张图片(顺序固定),并将其作为材质的 envMap(环境贴图)或 map(基础贴图) 核心注意事项

  • 图片顺序CubeTextureLoader 要求 6 张图片的顺序必须是:[posx, negx, posy, negy, posz, negz](与题目中的图片名对应:posx.jpgnegx.jpgposy.jpgnegy.jpgposz.jpgnegz.jpg);

  • 材质选择

  • 若仅展示贴图(无光照),用 MeshBasicMaterial
  • 若需光照效果(如反射、高光),用 MeshStandardMaterial 或 MeshPhysicalMaterial,并设置 metalness(金属度)和 roughness(粗糙度);
  • 图片路径:确保图片路径正确(如放在 public/textures/ 目录下)。 代码示例
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Threejs 立方体贴图</title>
  <!-- 引入 Threejs -->
  <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script>
    // 1. 创建场景
    const scene = new THREE.Scene();

    // 2. 创建相机(透视相机)
    const camera = new THREE.PerspectiveCamera(
      75, // 视场角(FOV)
      window.innerWidth / window.innerHeight, // 宽高比
      0.1, // 近裁剪面
      1000 // 远裁剪面
    );
    camera.position.z = 5; // 相机位置(远离立方体,避免遮挡)

    // 3. 创建渲染器
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染尺寸
    document.body.appendChild(renderer.domElement); // 将渲染结果添加到页面

    // 4. 加载 6 张立方体贴图
    const textureLoader = new THREE.CubeTextureLoader();
    // 图片路径(顺序:posx, negx, posy, negy, posz, negz)
    const cubeTextures = textureLoader.load([
      'textures/posx.jpg',
      'textures/negx.jpg',
      'textures/posy.jpg',
      'textures/negy.jpg',
      'textures/posz.jpg',
      'textures/negz.jpg'
    ]);

    // 5. 创建立方体几何体(宽、高、深均为 2)
    const geometry = new THREE.BoxGeometry(2, 2, 2);

    // 6. 创建材质(带光照效果的材质)
    const material = new THREE.MeshStandardMaterial({
      envMap: cubeTextures, // 设置环境贴图(立方体贴图)
      metalness: 0.8, // 金属度(0~1,越高越像金属)
      roughness: 0.2 // 粗糙度(0~1,越低越光滑)
    });

    // 7. 创建立方体网格(几何体 + 材质)
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube); // 将立方体添加到场景

    // 8. 添加光照(否则 MeshStandardMaterial 无法显示效果)
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // 环境光(柔和照亮所有物体)
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); // 平行光(模拟太阳光)
    directionalLight.position.set(5, 5, 5); // 平行光位置
    scene.add(ambientLight, directionalLight);

    // 9. 动画循环(让立方体旋转,便于观察 6 个面)
    function animate() {
      requestAnimationFrame(animate); // 循环调用 animate
      cube.rotation.x += 0.01; // x 轴旋转
      cube.rotation.y += 0.01; // y 轴旋转
      renderer.render(scene, camera); // 渲染场景和相机
    }
    animate(); // 启动动画

    // 10. 窗口大小自适应
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix(); // 更新相机投影矩阵
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>

14. 谈一下对文件过大,渲染卡顿的优化。

答案解析:

Threejs 渲染卡顿的核心原因是「模型 / 纹理文件过大」「渲染压力过高」「资源加载缓慢」,需从「模型优化」「纹理优化」「渲染优化」「加载优化」四个维度解决:

1. 模型优化(减少几何体复杂度)

  • 1.1 简化几何体

  • 使用 BufferGeometry 代替 GeometryBufferGeometry 是 Threejs 推荐的几何体类型,内存占用更低,渲染速度更快(Geometry 已废弃);

  • 合并顶点:用 BufferGeometryUtils.mergeVertices() 移除重复顶点,减少顶点数量(例:一个立方体默认 24 个顶点,合并后可减少到 8 个);

  • 减少面数:用 Blender/Maya 等建模工具删除模型的冗余面(如不可见的内部面),或使用「Decimate Modifier」(简化修改器)降低面数。

  • 1.2 使用 LOD(细节层次)
    为同一模型创建多个细节版本(高模、中模、低模),根据模型与相机的距离自动切换:

  • 远处:使用低模(面数少),减少渲染压力;

  • 近处:使用高模(面数多),保证视觉效果。
    示例:

const lod = new THREE.LOD();
// 高模(近)
const highGeo = new THREE.BoxGeometry(2, 2, 2, 100, 100, 100); // 细分多,面数多
// 低模(远)
const lowGeo = new THREE.BoxGeometry(2, 2, 2, 10, 10, 10); // 细分少,面数少
// 添加不同层级(距离相机 10 以内用高模,10~20 用低模)
lod.addLevel(new THREE.Mesh(highGeo, material), 0);
lod.addLevel(new THREE.Mesh(lowGeo, material), 10);
scene.add(lod);
  • 1.3 实例化(InstancedMesh)
    若场景中有大量相同模型(如森林中的树木、城市中的路灯),使用 InstancedMesh 复用一个几何体和材质,仅修改每个实例的位置 / 旋转 / 缩放,大幅减少 draw call(绘制调用):
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const count = 1000; // 1000 个实例
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

// 为每个实例设置位置
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
 matrix.setPosition(
    Math.random() * 100 - 50, // x 轴随机位置
    0,
  Math.random() * 100 - 50  // z 轴随机位置
  );
  instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // 动态更新标记
scene.add(instancedMesh);

2. 纹理优化(减少纹理内存占用)

  • 2.1 压缩纹理
    使用 GPU 支持的压缩纹理格式(如 Basis Universal、KTX2、DDS),压缩后的纹理体积仅为原体积的 1/4~1/8,且渲染时无需解压(直接由 GPU 处理),大幅减少内存占用和加载时间。
    Threejs 提供 BasisTextureLoader 加载压缩纹理:
import { BasisTextureLoader } from 'three/addons/loaders/BasisTextureLoader.js';
const loader = new BasisTextureLoader();
loader.setTranscoderPath('libs/basis/'); // 压缩纹理解码器路径
loader.detectSupport(renderer);
loader.load('textures/cube.basis', (texture) => {
  const material = new THREE.MeshStandardMaterial({ map: texture });
});
  • 2.2 降低纹理分辨率
    纹理分辨率需为「2 的幂次」(如 256x256、512x512,避免非幂次纹理导致的性能损耗),且根据场景需求选择合适分辨率:

  • 远景模型:用 256x256 以下纹理;

  • 近景模型:用 512x512~1024x1024 纹理(避免超过 2048x2048,除非必要)。

  • 2.3 使用纹理集(Texture Atlas)
    将多个小纹理合并为一张大纹理(如将立方体的 6 个面合并为一张纹理),减少纹理加载次数和 draw call(每个纹理需单独绑定 GPU,合并后仅需绑定一次)。

3. 渲染优化(减少 GPU 负担)

  • 3.1 减少 draw call

  • 合并几何体:用 BufferGeometryUtils.mergeBufferGeometries() 将多个相同材质的几何体合并为一个,减少绘制调用;

  • 使用纹理集:如上述「纹理优化」所述,减少纹理切换次数。

  • 3.2 优化光照和阴影

  • 减少光源数量:优先使用「环境光 + 1~2 个平行光」,避免使用多个点光源 / 聚光灯(每个光源需单独计算光照,开销大);

  • 优化阴影:

    • 关闭不必要的阴影(设置 mesh.castShadow = false/mesh.receiveShadow = false);
    • 缩小阴影贴图分辨率(directionalLight.shadow.mapSize.set(1024, 1024),避免 2048x2048 以上);
    • 限制阴影相机范围(directionalLight.shadow.camera.near/far/left/right),避免阴影计算范围过大。
  • 3.3 开启性能模式

  • 设置渲染器的 powerPreference: 'high-performance',优先使用高性能 GPU(适合桌面端);

  • 关闭抗锯齿(antialias: false),或使用 FXAA 后处理(抗锯齿效果更轻量):

    import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
    import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
    const fxaaPass = new ShaderPass(FXAAShader);
    const composer = new THREE.EffectComposer(renderer);
    composer.addPass(new THREE.RenderPass(scene, camera));
    composer.addPass(fxaaPass);
    // 渲染时用 composer.render() 代替 renderer.render()
    

4. 加载优化(减少加载时间和内存占用)

  • 4.1 分片加载(大模型)
    使用 GLTFLoader 加载 GLB/GLTF 格式模型,并开启 draco 压缩(模型体积减少 50%~70%),同时支持分片加载(将大模型拆分为多个小块,加载完成一块渲染一块):
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('libs/draco/'); // draco 解码器路径
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
// 加载模型(支持分片)
loader.load('models/large-model.glb', (gltf) => {
  scene.add(gltf.scene);
}, (xhr) => {
  // 加载进度
  console.log(`加载进度:${(xhr.loaded / xhr.total * 100).toFixed(0)}%`);
});
  • 4.2 懒加载(视口外资源)
    使用「视口检测」(如 three-addons 的 FrustumCulledObject),仅加载当前相机视口内的模型 / 纹理,视口外的资源延迟加载或不加载:
// 检测模型是否在视口内
const frustum = new THREE.Frustum();
const matrix = new THREE.Matrix4();
function isInView(mesh) {
  matrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
  frustum.setFromProjectionMatrix(matrix);
  return frustum.containsPoint(mesh.position);
}

// 懒加载逻辑
function lazyLoadMeshes(meshes) {
  meshes.forEach(mesh => {
    if (isInView(mesh) && !mesh.loaded) {
      // 加载模型并添加到场景
      loadMesh(mesh.url).then((loadedMesh) => {
        scene.add(loadedMesh);
        mesh.loaded = true;
      });
    }
  });
}
// 动画循环中调用懒加载
function animate() {
requestAnimationFrame(animate);
lazyLoadMeshes(allMeshes); // allMeshes 是所有待加载的模型列表
renderer.render(scene, camera);
}
  • 4.3 预加载关键资源
    优先加载首屏所需的核心资源(如相机附近的模型、主纹理),非核心资源(如远景模型)延后加载,确保首屏快速渲染。

5. 性能监控

优化前需先定位瓶颈,使用以下工具监控性能:

  • Stats.js:监控帧率(FPS)、CPU 使用率,判断是否卡顿;

  • Threejs 内置监控:用 renderer.info 获取 draw call 数量、三角形数量、纹理数量,定位高开销模块:

console.log('Draw Calls:', renderer.info.render.calls); // 绘制调用次数
console.log('Triangles:', renderer.info.render.triangles); // 三角形数量
console.log('Textures:', renderer.info.memory.textures); // 纹理数量