笔试题一
1. div+css 的布局较 table 布局有什么优点?
答案解析:
div+CSS 是结构与样式分离的布局方案,而 table 布局本质是为表格数据设计的,二者在布局场景下的核心差异如下,div+CSS 的优点主要体现在:
2. cookies、sessionStorage 和 localStorage 的区别:
答案解析:
特性 cookies sessionStorage localStorage 存储大小 较小(约 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 请求的缓存会经历「浏览器本地缓存 → 网络缓存 → 服务器缓存」的多层级处理,目的是减少重复请求、提升加载速度,具体阶段如下:
- 内存缓存(Memory Cache) - 优先级最高,浏览器将刚加载过的 JS(如当前会话内的请求)暂存到内存中,刷新页面时直接从内存读取,无需磁盘 I/O 和网络请求。 - 特点:速度最快,但内存有限,关闭标签页后缓存失效(内存释放)。
- 磁盘缓存(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 和新文件,更新磁盘缓存。- CDN 缓存
若 HTTP 缓存未命中(如首次请求或文件更新),请求会到达 CDN(内容分发网络): - CDN 节点会缓存常用 JS 文件,若节点已缓存且未过期,直接返回文件; - 若 CDN 未缓存或已过期,CDN 会向源服务器请求文件,缓存后再返回给用户(下次同区域用户可直接从 CDN 读取)。- 服务器端缓存
若 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:父元素与第一个 / 最后一个子元素
父元素没有padding、border、overflow: hidden等隔离时,父元素的margin-top会与第一个子元素的margin-top重叠;父元素的margin-bottom会与最后一个子元素的margin-bottom重叠,结果为「两者中的较大值」。
例:.parent { margin-top: 10px; } .child { margin-top: 20px; }
最终父元素整体的margin-top为20px(子元素的 margin 「穿透」父元素)。场景 3:空块级元素
一个无内容、无padding、border的空块级元素,其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 的优缺点
答案解析:
优点
- 上下文隔离:iframe 内的 HTML/CSS/JS 与父页面完全隔离,不会相互污染(如父页面的样式不会影响 iframe 内的元素,反之亦然),适合嵌入第三方内容(如广告、地图、支付页面)
- 异步加载不阻塞:iframe 内的资源加载不会阻塞父页面的解析和渲染(除非设置
async=false),可优化首屏加载速度。- 历史记录管理:通过
window.frames或postMessage可独立管理 iframe 内的历史记录(如前进 / 后退)。- 复用独立功能:将复杂功能(如富文本编辑器、视频播放器)封装为 iframe,可在多个页面复用,降低耦合。
缺点
性能开销大:
每个 iframe 会触发独立的 HTTP 请求和渲染流程,增加浏览器负担;
iframe 会阻塞父页面的
load事件(需等待 iframe 加载完成),可通过loading="lazy"优化。
- SEO 不友好:搜索引擎难以抓取 iframe 内的内容,若核心内容在 iframe 中,会影响页面排名。
- 跨域通信复杂:父页面与 iframe 间的通信需通过
postMessage(需处理跨域权限),无法直接访问对方的window对象。- 样式控制难: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 旧版本); - 部分设备(如无摄像头的电脑)会返回错误,需捕获并提示用户。
- 旧浏览器(如 IE)不支持该 API,需兼容时可使用
-
资源释放:使用完摄像头后,必须调用
stream.getTracks().forEach(track => track.stop())释放资源,避免占用设备内存。 -
隐私安全:开发者需明确告知用户摄像头的使用目的(如「用于视频通话」),遵守隐私法规(如 GDPR),不得未经允许录制用户画面。
10. 双向绑定原理
答案解析:
Vue 的双向绑定(
v-model)本质是「数据驱动视图 + 视图反馈数据」的闭环,核心依赖「数据劫持」和「依赖收集」,Vue 2 和 Vue 3 的实现方式有差异。Vue 2 实现:
Object.defineProperty()核心步骤:
数据劫持(Observer) :
Vue 初始化时,对data中的每个属性用Object.defineProperty()重写getter和setter:
getter:当属性被访问时(如模板渲染、JS 中读取),触发「依赖收集」;
setter:当属性被修改时(如this.msg = 'new'),触发「视图更新」。
依赖收集(Dep + Watcher) :
Dep(依赖容器) :每个被劫持的属性对应一个
Dep,用于存储依赖该属性的Watcher;Watcher(观察者) :模板编译时,每个绑定数据的 DOM 节点(如
{{ msg }})会生成一个Watcher,访问属性时触发getter,将Watcher加入Dep。
- 视图更新:
当属性被修改时,触发setter,Dep会通知所有关联的Watcher,Watcher调用「更新函数」,通过Virtual DOM的diff算法找到最小变更,更新真实 DOM。Vue 3 实现:
ProxyVue 2 的
Object.defineProperty()存在缺陷(无法监听对象新增 / 删除属性、数组索引修改),Vue 3 改用Proxy实现数据劫持,优势如下:
- 监听整个对象:无需递归劫持每个属性(
Proxy直接代理整个data对象),性能更优;- 支持更多场景:可监听对象新增 / 删除属性(如
this.obj.newKey = 'val')、数组索引修改(如this.arr[0] = 'new')、Array.prototype方法(如push/pop);- 返回代理对象:
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 描述」
对比维度 真实 DOM Virtual 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.jpg、negx.jpg、posy.jpg、negy.jpg、posz.jpg、negz.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代替Geometry:BufferGeometry是 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); // 纹理数量