此页面描述了常见的性能问题和缓解这些问题的最佳实践。
脚本计算
Luau 代码中的昂贵操作需要更长的处理时间,因此可能会影响帧率。除非以并行方式执行,否则 Luau 代码是同步运行的,并会阻塞主线程,直到遇到一个让线程暂停的函数。
常见问题
- 对表结构的密集操作 - 复杂操作如序列化、反序列化和深度克隆会产生高性能成本,尤其是在大型表结构上。如果这些操作是递归的或涉及迭代非常大的数据结构,这种情况尤其明显。
缓解
- 在 RunService 事件上谨慎调用代码,限制使用频率到高频调用是必需的情况(例如,更新相机)。您可以在其他事件中或在循环中以较低的频率执行大多数其他代码。
- 使用 task.wait() 将大型或昂贵的任务分解,以便在多个帧之间分散工作。
- 识别并优化不必要的昂贵操作,并对不需要访问数据模型的计算密集型任务使用 多线程。
- 某些服务器端脚本可以受益于 本机代码生成,这是一个简单的标志,可以将脚本编译为机器代码而不是字节码。
- 如果在优化后您的服务器仍然耗尽 CPU,您可以通过 扩展计算服务 预留更多资源。
MicroProfiler 范围
| 范围 | 相关计算 |
| RunService.PreRender | 在 PreRender 事件上执行的代码 |
| RunService.PreSimulation | 在 Stepped 事件上执行的代码 |
| RunService.PostSimulation | 在 Heartbeat 事件上执行的代码 |
| RunService.Heartbeat | 在 Heartbeat 事件上执行的代码 |
有关使用 MicroProfiler 调试脚本的更多信息,请参阅 debug 库,其中包括用于标记特定代码和进一步增加特异性的函数,例如 debug.profilebegin 和 debug.profileend。许多脚本调用的 Roblox API 方法也有其自己的相关 MicroProfiler 标签,可以提供有用的信号。
脚本内存使用
当您编写的脚本消耗内存而垃圾收集器无法正确释放时,可能会发生内存泄漏。泄漏在服务器上尤其普遍,因为它们可以持续在线多天,而客户端会话则短得多。
开发者控制台中的以下内存值可能表明需要进一步调查的问题:
- LuaHeap - 高或增长的消耗表明可能存在内存泄漏。
- InstanceCount - 实例数量持续增长表明您的代码中对某些实例的引用未被垃圾收集。
- PlaceScriptMemory - 提供逐脚本的内存使用情况细分。
常见问题
保持连接的连接 - 引擎从不垃圾收集连接到实例的事件及其连接回调中引用的任何值。因此,事件的活动连接和连接实例、连接函数及引用值中的代码,即使在事件触发后,也超出了内存垃圾收集器的范围。
尽管当它们所属的实例被销毁时,事件会断开连接,但一个常见的错误是认为这适用于 Player 对象。在用户离开游戏后,引擎不会自动销毁他们的代表 Player 对象和角色模型,因此如果您不在脚本中断开与 Player 对象及角色模型下的实例(如 CharacterAdded)的连接,它们仍会消耗内存。随着数百名用户加入和离开游戏,这可能会导致服务器上非常显著的内存泄漏。
表 - 将对象插入表中但在不再需要时不将其移除会导致不必要的内存消耗,尤其是对于跟踪用户数据的表。例如,以下代码示例在每次用户加入时创建一个添加用户信息的表:
示例local playerInfo = {}Players.PlayerAdded:Connect(function(player)playerInfo[player] = {} -- 一些信息end)如果您不在不再需要时移除这些条目,表的大小将继续增长,并随着更多用户加入会消耗更多内存。任何迭代此表的代码在表增长时也会变得计算上更昂贵。
缓解
要清理所有使用的值以防止内存泄漏:
断开所有连接 - 检查您的代码库,确保每个连接都通过以下路径之一进行清理:
- 使用 Disconnect() 函数手动断开连接。
- 使用 Destroy() 函数销毁事件所属的实例。
- 销毁连接回溯到的脚本对象。
在离开后移除玩家对象和角色 - 启用 Workspace.PlayerCharacterDestroyBehavior 以在用户离开后自动销毁玩家对象和角色模型。如果您愿意,也可以手动清理它们:
示例玩家和角色清理local Players = game:GetService("Players")Players.PlayerAdded:Connect(function(player)player.CharacterRemoving:Connect(function(character)task.defer(character.Destroy, character)end)end)Players.PlayerRemoving:Connect(function(player)task.defer(player.Destroy, player)end)
物理计算
过度的物理模拟可能是服务器和客户端每帧计算时间增加的主要原因。
常见问题
过度的物理时间步频率 - 默认情况下,步进行为处于 自适应模式,物理以 60 Hz、120 Hz 或 240 Hz 的频率步进,具体取决于物理机制的复杂性。
还提供了一种固定模式,具有更高的物理精度,强制所有物理组件以 240 Hz(每帧四次)步进。这会导致每帧计算显著增加。
模拟对象的复杂性过高 - 模拟的 3D 组件越多,每帧的物理计算所需的时间就越长。通常,游戏中会有不需要模拟的对象,或者会有比所需更多的约束和关节的机制。
过于精确的碰撞检测 - 网格部件具有 CollisionFidelity 属性用于检测碰撞,提供不同性能影响级别的多种模式。网格部件的精确碰撞检测模式具有最高的性能成本,并且需要更长的时间进行计算。
缓解
锚定不需要模拟的部件 - 锚定所有不需要物理驱动的部件,例如静态 NPC。
使用自适应物理步进 - 自适应步进动态调整物理计算的速率,允许在某些情况下减少物理更新的频率。
减少机制复杂性
- 在可能的情况下,最小化组件中的物理约束或关节数量。
- 减少机制内的自我碰撞,例如通过对布娃娃肢体施加限制或无碰撞约束以防止它们相互碰撞。
减少网格的精确碰撞保真度的使用
对于小型或不可交互的对象,用户很少注意到差异,使用盒子保真度。
对于小到中型对象,根据形状使用盒子或外壳保真度。
对于大型和非常复杂的对象,尽可能使用不可见部件构建自定义碰撞。
对于不需要碰撞的对象,禁用碰撞并使用盒子或外壳保真度,因为碰撞几何仍然存储在内存中。
有关如何选择平衡精度和性能要求的碰撞保真度选项的详细说明,请参阅 设置物理和渲染参数。
MicroProfiler 范围
| 范围 | 相关计算 |
| physicsStepped | 整体物理计算 |
| worldStep | 每帧进行的离散物理步骤 |
物理内存使用
物理运动和碰撞检测会消耗内存。网格部件具有 CollisionFidelity 属性,决定用于评估网格的碰撞边界的方法。
常见问题
默认和精确的碰撞检测模式消耗的内存显著高于其他两个低保真度碰撞形状的模式。
如果您在 PhysicsParts 下看到高水平的内存消耗,您可能需要考虑减少游戏中对象的 碰撞保真度。
如何缓解
要减少碰撞保真度使用的内存:
- 使用 CollisionFidelity 设置减少碰撞的保真度。Box 的内存开销最低,而 Default 和 Precise 通常更昂贵。
- 通常可以安全地将任何小型锚定部件的碰撞保真度设置为 Box。
- 对于非常复杂的大型网格,您可能希望使用较小的对象构建自己的碰撞网格,使用盒子碰撞保真度。
人形
Humanoid 是一个为玩家和非玩家角色(NPC)提供广泛功能的类。尽管功能强大,但 Humanoid 的计算成本相当高。
常见问题
- 在 NPC 上启用所有 HumanoidStateTypes - 保留某些 HumanoidStateTypes 启用会产生性能成本。禁用 NPC 不需要的任何状态。例如,除非您的 NPC 要爬梯子,否则可以安全地禁用 Climbing 状态。
- 在不需要的情况下使用人形 - 静态 NPC 通常不需要 Humanoid 类。
- 从服务器播放大量 NPC 动画 - 在服务器上运行的 NPC 动画需要在服务器上进行模拟并复制到客户端。这可能是多余的开销。
- 执行不必要的大小和缩放更改 - 大小/缩放更改会导致 FastCluster 被重建。如果您看到与 FastCluster 相关的性能问题,请尽量减少此类更改。同样,其他属性更改也可能导致 FastCluster 被重建,因此通常尽量减少这些更改。
缓解
- 在客户端播放 NPC 动画 - 在有大量 NPC 的游戏中,考虑在客户端创建 Animator 并在本地运行动画。这减少了服务器的负担,并减少了不必要的复制需求。它还使额外的优化成为可能(例如,仅为靠近角色的 NPC 播放动画)。
- 使用性能友好的替代人形 - NPC 模型不一定需要包含人形对象。
- 对于静态 NPC,使用简单的 AnimationController,因为它们不需要移动,只需播放动画。
- 对于移动的 NPC,考虑实现自己的移动控制器,并根据 NPC 的复杂性使用 AnimationController 进行动画。
- 禁用未使用的人形状态 - 使用 Humanoid:SetStateEnabled() 仅启用每个人形所需的状态。
- 对频繁重生的 NPC 模型进行池化 - 不要完全销毁 NPC,而是将 NPC 发送到一个非活动 NPC 池中。这样,当需要重生新的 NPC 时,您可以简单地重新激活池中的一个 NPC。这个过程称为池化,最小化了角色需要实例化的次数。
- 仅在用户附近生成 NPC - 当用户不在范围内时,不要生成 NPC,并在用户离开范围时将其剔除。
- 避免在实例化后对头像层次结构进行更改 - 对头像层次结构的某些修改会产生显著的性能影响。一些优化是可用的:
MicroProfiler 范围
| 范围 | 相关计算 |
| stepHumanoid | 人形控制和物理 |
| stepAnimation | 人形和动画师动画 |
| updateInvalidatedFastClusters | 与实例化或修改头像相关 |
渲染
客户端每帧花费的时间中很大一部分用于渲染当前帧中的场景。服务器不进行任何渲染,因此本节仅适用于客户端。
绘制调用
绘制调用是引擎向 GPU 发出的渲染某物的一组指令。绘制调用具有显著的开销。通常,每帧的绘制调用越少,渲染一帧所花费的计算时间就越少。
您可以通过 Studio 中的 渲染统计 ⟩ 计时 项查看当前发生的绘制调用数量。您可以通过按 ShiftF2 在客户端查看 渲染统计。
在给定帧中需要绘制的对象越多,向 GPU 发出的绘制调用就越多。然而,Roblox 引擎利用一种称为 实例化 的过程,将具有相同纹理特征的相同网格合并为一个绘制调用。具体而言,当多个网格具有相同的 MeshContent 时,它们在单个绘制调用中处理:
- 如果存在,SurfaceAppearances 相同,否则当 TextureContents 相同。
- 当 SurfaceAppearance 和 MeshPart.TextureID 都不存在时,材料相同。
其他常见问题
过度的对象密度 - 如果大量对象集中在高密度区域,则渲染该区域的场景需要更多的绘制调用。如果您发现查看地图的某个部分时帧率下降,这可能是一个很好的信号,表明该区域的对象密度过高。
像贴花、纹理和粒子这样的对象不易批处理,并会引入额外的绘制调用。特别注意场景中的这些对象类型。特别是,对 ParticleEmitters 的属性更改可能会对性能产生显著影响。
错过实例化机会 - 通常,场景中会包含相同的网格重复多次,但每个网格的网格或纹理资产 ID 不同。这会阻止实例化并导致不必要的绘制调用。
这个问题的一个常见原因是整个场景一次性导入,而不是将单个资产导入到 Roblox 中,然后在导入后重复使用以组装场景。
甚至像这样的简单脚本也可以帮助您识别使用不同网格 ID 的相同名称的网格部件:
for _,descendant in workspace:GetDescendants() doif descendant:IsA("MeshPart") thenprint(descendant.Name .. ", " .. descendant.MeshId)endend输出(启用 堆栈行)可能看起来像这样。重复的行表示重用相同的网格,这是好的。唯一的行不一定是坏的,但根据您的命名方案,可能表明游戏中存在重复的网格:
LargeRock, rbxassetid://106420009602747 (x144) -- goodLargeRock, rbxassetid://120109824668127LargeRock, rbxassetid://134460273008628LargeRock, rbxassetid://139288987285823LargeRock, rbxassetid://71302144984955LargeRock, rbxassetid://90621205713698LargeRock, rbxassetid://113160939160788LargeRock, rbxassetid://135944592365226 -- all possible duplicates过度的对象复杂性 - 尽管绘制调用的数量不如重要,但场景中的三角形数量确实会影响渲染一帧所需的时间。具有非常大量非常复杂网格的场景是一个常见问题,过多的网格上设置 MeshPart.RenderFidelity 属性为 Precise 也是如此。
过度的阴影投射 - 处理阴影是一个昂贵的过程,包含大量和高密度的光源对象(或受到阴影影响的小部件)的地图可能会出现性能问题。
高透明度过度绘制 - 将部分透明的对象放置在彼此附近会迫使引擎多次渲染重叠的像素,这可能会影响性能。有关识别和修复此问题的更多信息,请参阅 删除分层透明度。
不必要的蒙皮 MeshPart 移动 - 没有 Humanoid 的模型中的蒙皮 MeshParts 使用空间组织的 FastClusters 进行分组。当这些 MeshParts 移动时,它们必须不断地添加到这些空间集群中并从中移除,迫使集群重建并影响性能。
- 一个非常有效的解决方法是在模型中嵌入一个 Humanoid。Humanoid 的存在会覆盖默认的空间聚类行为,强制整个模型使用一个统一的 FastCluster。因此,位置更新不再需要重建集群,从而缓解性能瓶颈。此技术应仅保留给预期移动的 MeshParts,因为它可能会引入内存开销并抵消空间优化的好处。我们建议在进行这些类型的更改后始终对您的游戏进行性能分析。有关更多信息,请参阅 人形性能提示。
在 Model 中的部件过多 - 在模型中部件过多可能会导致由于部件属性变化而更频繁地重建。找到在使用 FastCluster 时模型中部件的适当平衡。
缓解
- 实例化相同的网格并减少唯一网格的数量 - 如果您确保所有相同的网格具有相同的基础资产 ID,引擎可以识别并在单个绘制调用中渲染它们。确保每个网格在地图中仅上传一次,然后在 Studio 中重复使用,而不是将大型地图作为整体导入,这可能导致相同的网格具有不同的内容 ID,并被引擎识别为唯一资产。包 是对象重用的有用机制。
- 剔除 - 剔除描述了消除不影响最终渲染帧的对象的绘制调用的过程。默认情况下,引擎跳过相机视野外的对象的绘制调用(视锥剔除)以及被其他对象遮挡的部件、网格和地形(遮挡剔除)。在某些情况下,例如室内环境,您可能能够实现房间或传送门系统,并手动剔除对象以进一步减少绘制调用或整体计算负载。
- 在适当的部件和光源对象上禁用阴影投射 - Roblox 引擎会随着客户端图形质量级别的降低自动降低阴影质量,最终在质量级别低于 4 时完全禁用阴影。但是,您可以选择性地在光源对象和部件上禁用阴影投射属性,以提高性能,同时启用阴影,并增加阴影保持启用的可能性。您可以在编辑时或在运行时动态进行的一些优化示例:
使用 BasePart.CastShadow 属性在不太可能可见阴影的小部件上禁用阴影投射。当应用于远离用户相机的部件时,这种策略特别有效。
尽可能在移动对象上禁用阴影。
在不需要投射阴影的光源实例上禁用 Light.Shadows。
限制光源实例的范围和角度。
使用更少的光源实例。
考虑在特定范围外或逐房间禁用光源,特别是在室内环境中。
MicroProfiler 范围
| 范围 | 相关计算 |
| Prepare and Perform | 整体渲染 |
| Perform/Scene/computeLightingPerform | 光网格和阴影更新 |
| LightGridCPU | 体素光网格更新 |
| ShadowMapSystem | 阴影映射 |
| Perform/Scene/UpdateView | 渲染准备和粒子更新 |
| Perform/Scene/RenderView | 渲染和后处理 |
网络和复制
网络和复制描述了数据在服务器和连接的客户端之间发送的过程。每帧在客户端和服务器之间发送信息,但较大的信息量需要更多的计算时间。
常见问题
过度的远程流量 - 通过 RemoteEvent 或 RemoteFunction 对象发送大量数据或非常频繁地调用它们可能导致每帧处理传入数据包消耗大量 CPU 时间。常见错误包括:
- 每帧复制不需要复制的数据。
- 在用户输入时复制数据而没有任何限制机制。
- 发送超过所需的数据。例如,在购买物品时发送玩家的整个库存,而不是仅发送所购物品的详细信息。
复杂实例树的创建或删除 - 当对服务器上的数据模型进行更改时,它会复制到连接的客户端。这意味着在运行时创建和销毁大型实例层次结构(如地图)可能会非常耗费网络资源。
这里的一个常见罪魁祸首是 动画编辑器 插件在模型中保存的复杂动画数据。如果在游戏发布之前未将其删除,并且动画模型被定期克隆,则会不必要地复制大量数据。
服务器端 TweenService - 如果在服务器端使用 TweenService 对对象进行插值,则每帧都会将插值的属性复制到每个客户端。这不仅会导致由于客户端延迟波动而使插值变得抖动,还会导致大量不必要的网络流量。
缓解
您可以采用以下策略来减少不必要的复制:
- 避免通过远程事件一次性发送大量数据。相反,仅以较低的频率发送必要的数据。例如,对于角色的状态,在状态更改时复制,而不是每帧复制。
- 将复杂的实例树分块,例如地图,并分段加载它们,以便在多个帧之间分配复制工作。
- 清理动画元数据,特别是模型的动画目录,在导入后。
- 限制不必要的实例复制,尤其是在服务器不需要了解正在创建的实例的情况下。这包括:
- 视觉效果,例如爆炸或魔法法术爆发。服务器只需要知道位置以确定结果,而客户端可以在本地创建视觉效果。
- 第一人称物品视图模型。
- 在客户端而不是服务器上对对象进行插值。
MicroProfiler 范围
| 范围 | 相关计算 |
| ProcessPackets | 处理传入网络数据包,例如事件调用和属性更改 |
| Allocate Bandwidth and Run Senders | 与服务器相关的输出事件 |
资产内存使用
提高客户端内存使用的最高影响机制是启用 实例流。
实例流
实例流选择性地加载不需要的数据模型部分,这可以显著减少加载时间,并提高客户端在内存压力下防止崩溃的能力。
如果您遇到内存问题并且实例流已禁用,请考虑更新您的游戏以支持它,特别是如果您的 3D 世界很大。实例流基于 3D 空间中的距离,因此较大的世界自然受益更多。
如果启用了实例流,您可以增加其激进性。例如,考虑:
- 尽可能减少使用 Enum.ModelStreamingMode.Persistent。如果您将其用作兼容性措施,可能需要更新您的脚本。
- 减少 Workspace.StreamingMinRadius 和 Workspace.StreamingTargetRadius。
有关流选项及其好处的更多信息,请参阅 流属性。
其他常见问题
资产重复 - 一个常见错误是多次上传相同的资产,导致不同的资产 ID。这可能导致相同的内容多次加载到内存中。
资产数量过多 - 即使资产不相同,也有可能错过重用相同资产以节省内存的机会。
音频文件 - 音频文件可能是内存使用的一个意外贡献者,特别是如果您一次性将所有音频文件加载到客户端,而不是仅加载游戏的一部分所需的内容。有关策略,请参阅 加载时间。
高分辨率纹理 - 纹理的图形内存消耗与纹理在磁盘上的大小无关;纹理中的像素数量决定了内存使用。例如,1024x1024 像素的纹理消耗的图形内存是 512x512 纹理的四倍。
上传到 Roblox 的图像会被转码为固定格式,因此在上传与每个像素相关的字节数较少的颜色模型的图像时没有内存好处。同样,在上传之前压缩图像或从不需要的图像中删除 alpha 通道可以减少图像在磁盘上的大小,但不会改善内存使用。
当游戏加载时,引擎会自动从较低质量的纹理开始,然后根据可用设备内存、与相机的距离、纹理占用的屏幕空间量和其他因素逐步提高质量。即便如此,战略性地调整纹理大小可以改善游戏中的内存使用。
缓解
仅上传资产一次 - 在对象之间重用相同的资产 ID,并确保相同的资产,特别是网格和图像,不要单独多次上传。
查找并修复重复资产 - 查找多次上传的相同网格部件和纹理,使用不同的 ID。
- 尽管没有 API 可以自动检测资产的相似性,但您可以收集您地点中的所有图像资产 ID(手动或使用脚本),下载它们,并使用外部比较工具进行比较。
- 对于网格部件,最佳策略是将唯一的网格 ID 按大小组织,以手动识别重复项。
- 不要为不同颜色使用单独的纹理,而是上传单个纹理,并使用 SurfaceAppearance.Color 属性应用各种色调。
单独导入地图资产 - 不要一次性导入整个地图,而是单独导入和重建地图中的资产。导入器不会对网格进行去重,因此如果您导入一个包含许多单独地板瓷砖的大型地图,每个瓷砖都会作为单独的资产导入(即使它们是重复的)。这可能会导致性能和内存问题,因为每个网格都被视为单独的,并占用内存和绘制调用。
限制图像的像素,不超过必要的数量。除非图像占据屏幕上的大量物理空间,否则通常最多需要 512x512 像素。大多数小图像应小于 256x256 像素。
使用修整图纸,以确保在 3D 地图中最大限度地重用纹理。有关如何创建修整图纸的步骤和示例,请参阅 创建修整图纸。
您还可以考虑使用精灵图纸将许多较小的 UI 图像作为单个图像加载。然后,您可以使用 ImageLabel.ImageRectOffset 和 ImageLabel.ImageRectSize 显示图纸的部分内容。
加载时间
在玩家开始游戏之前,客户端必须接收 ReplicatedFirst 和 ReplicatedStorage 中的所有内容、Workspace 的初始内容以及您的脚本预加载的任何资产。客户端在此期间必须下载和处理的内容越多,玩家等待的时间就越长。有关加入过程每个步骤的详细信息,请参阅 识别性能问题。
常见问题
ReplicatedStorage 中的内容过多 - ReplicatedStorage 中的所有内容在玩家可以看到或与任何内容交互之前都会复制到每个客户端,无论客户端是否立即需要它。免费模型和继承资产可能会悄悄增加显著的负担。
重的 ReplicatedFirst 内容 - ReplicatedFirst 在其他任何内容之前复制,主要用于保存自定义加载屏幕。每个额外的资产或脚本都会延迟主复制阶段的开始。
过度使用 PreloadAsync() - 许多游戏实现自定义加载屏幕,并使用 ContentProvider:PreloadAsync() 请求资产,以便在后台下载图像、声音和网格。这种方法确保游戏的重要部分完全加载而不会出现突入,但一个常见错误是预加载的资产超过实际所需的数量。
一个不良实践的例子是加载整个 Workspace。虽然这可能防止纹理突入,但它显著增加了加载时间。
另一个类似的做法是利用 ContentProvider.RequestQueueSize 确保所有请求的资产都已完成加载。然而,这会导致加载时间显著增加,同时由于其波动性,这也是一种不可靠的方法。
昂贵的启动脚本 - 在加入过程中运行的 LocalScripts 中的重脚本执行,例如同步加载大型数据结构,会延迟从复制到可玩状态的过渡。
缓解
启用实例流 - 流式传输仅在玩家初次加入时发送靠近玩家出生点的世界部分,然后随着玩家在游戏中移动逐步流入其余部分。对于尚未使用它的游戏,流式传输是减少加载时间的最有效方法之一。新地点默认启用流式传输。有关设置步骤、属性参考和迁移指导,请参阅 实例流。
保持 ReplicatedStorage 精简 - 仅存储客户端在加入时所需的内容,例如 ModuleScripts 的共享内容,这些内容是 LocalScripts 所需的。将不立即需要的大型模型、音频和其他资产移动到 ServerStorage,并在玩家需要时再移动到客户端。定期审计 ReplicatedStorage,以查找您不再使用的内容。
保持 ReplicatedFirst 精简 - 将 ReplicatedFirst 限制为加载屏幕所需的内容,例如基本 UI 元素,而不是大型纹理或 3D 模型,并避免在从中运行的脚本中进行重计算。有关构建自定义加载屏幕的详细说明,包括何时调用 ReplicatedFirst:RemoveDefaultLoadingScreen(),请参阅 加载屏幕。
仅预加载必要的资产 - 仅在必要情况下使用 ContentProvider:PreloadAsync(),包括:
- 加载屏幕中的图像。
- 游戏菜单中重要的图像,例如按钮背景和图标。
- 起始或生成区域中的重要资产。
如果您必须加载大量资产,建议提供一个 跳过加载 按钮。
推迟非必要的启动工作 - 仅在首次需要时加载玩家尚未到达的内容,例如他们尚未打开的 UI 面板。对于必须尽早发生的初始化工作,将其分散到多个帧中,而不是一次性运行。有关技术,请参阅 脚本计算。