首页/新闻资讯/正文详情

Three.js 场景加载时,为什么连 Loading 都会卡?一次 3D 钓场的首帧优化

发布时间:2026/11/5 23:16:25 来源:云帆数科 栏目:资讯中心
Three.js 场景加载时,为什么连 Loading 都会卡?一次 3D 钓场的首帧优化
最近在做一个电脑端三维垂钓网站「闲钓小栈」。进入钓场时要准备岸边地形、树木、人物、水面反射和阴影。开发时遇到一个问题加载提示已经出现画面还是会停一下。资源下载结束后也不一定能马上进入场景。这次我主要处理的是首次进入场景时CPU 计算、纹理上传和着色器编译集中执行的问题。做法是把准备过程拆开让浏览器在部分工作之间有机会更新界面并提前处理当前场景需要的渲染程序。项目使用 TypeScript 和 Three.js本文对应的 Three.js 版本是0.186.1。一、资源下载完成还不是首帧准备完成我把进入钓场的工作拆成了几部分阶段实际工作资源读取下载模型和贴图解析模型数据CPU 准备计算地形高度、坡度、顶点颜色整理场景物件纹理初始化把贴图准备并上传给 GPU程序准备编译、链接当前材质和渲染路径需要的着色器实际绘制使用正式相机绘制主场景、水面反射及阴影有些资源请求可以和地形计算重叠进行但主线程上的大循环不会因为外面包了一个async函数就自动并行。如果大量工作一直占着主线程页面上的等待动画、按钮和文字更新也会受到影响。只增加一个旋转图标解决不了这些初始化开销。二、先把地形计算拆成短一些的片段场景里同一个地形高度函数同时用于渲染和人物站位。生成网格时每个顶点还会计算坡度和颜色连续执行会占用不少时间。我的实现每处理 256 个顶点检查一次耗时超过约 8ms 就暂停当前计算等待下一次动画帧回调。下面只保留高度更新部分import*asTHREEfromthree;constnextFrame()newPromisevoid(resolverequestAnimationFrame(()resolve()));asyncfunctionprepareHeights(geometry:THREE.PlaneGeometry,heightAt:(x:number,z:number)number,cancelled:()boolean,){constpositionsgeometry.attributes.position;letstartedAtperformance.now();for(leti0;ipositions.count;i){if(i%2560performance.now()-startedAt8){awaitnextFrame();if(cancelled())returnfalse;startedAtperformance.now();}positions.setY(i,heightAt(positions.getX(i),positions.getZ(i)));}if(cancelled())returnfalse;positions.needsUpdatetrue;geometry.computeVertexNormals();returntrue;}这里的 8ms 是软预算。检查之间的计算、单次高度采样以及最后的法线计算都可能超出它不能据此声称所有任务都小于 8ms。requestAnimationFrame的回调发生在下一次重绘之前。这里用它把计算分散到不同的帧调度周期不把它当作“这一帧已经显示到屏幕”的证明后台标签页还可能暂停这类回调。requestAnimationFrame 的调度说明这一步仍然使用主线程。如果后续测量发现地形计算持续占主导再评估 Worker 和数据传输成本不能把分片描述成已经完成了多线程计算。三、把首次纹理初始化分散开另一个集中开销来自贴图图片文件已经读取完成也不代表 GPU 侧的纹理已经准备好。我先遍历当前场景收集材质上的纹理引用用Set去重然后逐个初始化中间让出执行机会asyncfunctionuploadTextures(renderer:THREE.WebGLRenderer,textures:IterableTHREE.Texture,cancelled:()boolean,){for(consttextureofnewSet(textures)){if(cancelled())returnfalse;renderer.initTexture(texture);awaitnextFrame();}return!cancelled();}initTexture是 Three.js 提供的公开接口可以提前初始化纹理减少第一次渲染时才集中处理图片解码和上传的开销。Three.js 纹理初始化说明项目收集的是普通材质直接引用的贴图以及ShaderMaterial中直接作为 uniform 值的纹理。纹理数组等其他结构需要按实际材质另外补充收集规则。这段代码也没有让单张纹理上传变成异步任务。如果某一张图片本身就很大单次调用仍可能阻塞需要继续检查尺寸、格式和解码成本。四、着色器预热要考虑实际渲染路径把纹理上传移开之后还需要处理第一次使用材质时的程序编译。我用renderer.compileAsync(scene, camera)预先准备程序。它利用KHR_parallel_shader_compile扩展提供的能力等待编译状态调用前场景里的灯光、环境和材质要先配置好。Three.js 着色器预编译说明这个扩展主要帮助避免阻塞式查询编译完成状态不意味着编译总耗时消失也不能保证所有浏览器、驱动和调用阶段都完全不阻塞。KHR_parallel_shader_compile 扩展说明闲钓场景里比较容易漏掉的是主画面、水面反射和阴影并不完全走同一条材质程序路径。我的主画面输出到 Canvas水面反射使用离屏渲染目标。核对当前 Three.js 源码后可以看到输出环境参与程序参数选择。因此我在主场景预编译后还切换到一个小的离屏目标准备对应的线性输出路径asyncfunctioncompileMainAndOffscreen(renderer:THREE.WebGLRenderer,scene:THREE.Scene,camera:THREE.Camera,cancelled:()boolean,){if(cancelled())returnfalse;awaitrenderer.compileAsync(scene,camera);if(cancelled())returnfalse;constpreviousrenderer.getRenderTarget();constwarmTargetnewTHREE.WebGLRenderTarget(1,1);try{renderer.setRenderTarget(warmTarget);awaitrenderer.compileAsync(scene,camera);return!cancelled();}finally{renderer.setRenderTarget(previous);warmTarget.dispose();}}这里的 1×1 目标用于切换当前项目所需的编译环境没有绘制一张完整反射图也没有让正式反射的运行开销变成一个像素。它同样不是所有离屏配置都适用的通用预热方案。阴影还需要另外考虑。树叶这类带透明裁切的材质如果深度路径没有沿用贴图、裁切阈值和正确的面方向阴影程序就可能与主材质不同。当前实现会为相关投影物件建立临时的深度材质代理沿用map、alphaMap、alphaTest、面方向和位移等参数再进行深度路径预编译。临时材质保持引用等第一次正式阴影绘制之后再释放。这部分针对的是项目当前使用的阴影和材质。以后增加点光源、定制深度材质、不同反射相机或后处理还要重新检查首次使用的路径不能认为调用过一次compileAsync就预热了所有可能性。五、准备期间正常渲染循环也要等一等如果一边分片准备场景一边让正常循环持续绘制尚未准备完成的物件第一次渲染仍可能提前触发纹理上传和着色器编译。所以世界运行器里有一个preparing状态。准备期间先不进入正常场景更新与渲染显示已有封面和 DOM/CSS 入场动画模型、地形和材质准备完成后再用正式相机实际绘制。当前进入流程可以概括为先显示入场界面 ↓ 启动资源读取并分片计算地形 ↓ 组装人物、植被、装备和水面 ↓ 逐步初始化纹理预编译当前渲染路径 ↓ 使用正式相机实际绘制检查上下文状态 ↓ 结束准备恢复正常循环和操作资源加载失败或 WebGL 上下文丢失就进入失败和重试流程。用户退出等待时异步步骤通过disposed检查停止继续使用已销毁的世界临时渲染目标和材质仍要释放。这里的“准备好”是应用完成了预定准备并走过实际绘制路径不是浏览器提供的“所有 GPU 工作都结束、画面已经呈现”的同步保证。六、看长任务也保留没有解决完的问题检查时我同时看入场动画是否仍有变化、资源失败能否重试以及主线程的长任务。在支持 Long Tasks API 的浏览器里可以用下面的方式记录任务时长constdurations:number[][];if(PerformanceObserver.supportedEntryTypes.includes(longtask)){constobservernewPerformanceObserver(list{for(constentryoflist.getEntries())durations.push(entry.duration);});observer.observe({type:longtask,buffered:false});// 采样结束后调用 observer.disconnect()。}Long Tasks API 记录的是占用 UI 线程约 50ms 及以上的任务不是 GPU 耗时也不能单靠这个数字判断稳定帧率。Long Tasks API 说明项目保留的一次开发检查使用 Mac Chrome、1440×960 视口和 4 倍 CPU 放慢。该次检查主动延迟了 GLB 资源用来验证等待中的动画和准备条件记录中的最大主线程长任务仍有616ms。这个结果说明初始化仍有集中开销不能写成“加载已经完全不卡”。同样人为延迟资源后的总等待时间也不能当成自然网络下的场景加载速度。后续还需要按 Performance 记录继续定位剩余任务并在目标设备验证。当前开发机是 Apple M5 Pro不能用它代替普通集显电脑的首次加载和运行测试。这次实际做的是把首次准备整理成可调度、可取消、能失败恢复的流程。对我来说后面再增加场景、贴图或新效果时也更容易知道该在哪个阶段继续排查。这套实现用在我开发的闲钓小栈中目前是电脑端单人三维垂钓与学习预览。体验地址闲钓小栈。

相关推荐

从 LLM 到 Agent Skill:大模型基础从 0 理解 Agent
从 LLM 到 Agent Skill:大模型基础从 0 理解 Agent

目录一、LLM:大语言模型二、大模型工作原理三、Token和Tokenizer四、Context:大模型的临时记忆体五、RAG:突破上下文限制的技术六、提示词(Prompt)七、Function Calling:大模型外部能力八、MCP:… · 2026/11/3 21:31:10

CC工具箱使用指南:【地块编号自动重排】
CC工具箱使用指南:【地块编号自动重排】

一、简介还是学湘源的一个工具,给地块进行自动排编号。二、工具参数介绍点击【控规】组里的【地块编号自动重排】工具:即可打开下面的工具框界面:1、地块图层要处理的地块图层。2、编号字段生成的地块编号会写入到这里。3、编号前缀、起始编号… · 2026/11/2 18:29:39

虚拟机从ESXI迁移至PVE全过程保姆记录
虚拟机从ESXI迁移至PVE全过程保姆记录

目录 第一部分 安装PVE系统 一、下载PVE安装镜像 二、制作PVE安装U盘 三、安装PVE系统 四、PVE环境配置 第一项:更换PVE软件源 第二项:配置网桥 五、从Esxi迁移虚拟机 版本背景: ESXI版本为6.7.0 ;PVE版本为9.2-1 &… · 2026/11/2 18:07:12

EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析
EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析

1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被… · 2026/11/5 16:49:35

Rust原来很简单二:所有权系统
Rust原来很简单二:所有权系统

Part 2 核心篇 —— 所有权系统 本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理 6… · 2026/11/5 16:26:08

基于Hadoop+Spark的民宿智能推荐系统设计与实现
基于Hadoop+Spark的民宿智能推荐系统设计与实现

1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享… · 2026/11/5 14:59:25

ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计
ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41

UART接口24位ADC搭配RS485,解决远距离温度监控难题
UART接口24位ADC搭配RS485,解决远距离温度监控难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41

UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践
UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07

多智能体协作系统架构设计与实践优化
多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39

大模型系统性入门实战指南:从原理到部署微调的全链路路线
大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39

鸿蒙文件系统沙箱机制解析与开发实践
鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码