Svelte 4/5内存泄漏问题排查:简易示例中的异常分析
Svelte 4/5内存泄漏问题:数组shift操作导致的实例残留
问题复现代码
<script lang="ts"> class Circle { cx: number; cy: number; constructor(clientX: number, clientY: number) { this.cx = +clientX.toFixed(); this.cy = +clientY.toFixed(); } } let circles = $state<Circle[]>([]); function addCircles() { for (let i = 0; i < 2; i++) { circles.push(new Circle(Math.random() * 600, Math.random() * 400)); } } function deleteCircle() { circles.shift(); } </script> <div class="space-y"> <div class="actions flex-center"> <button onclick={addCircles}>Add 2 Circles</button> <button onclick={deleteCircle}>Delete Circle</button> </div> <svg viewBox="0 0 600 400"> {#each circles as circle} <circle {...circle} r={40} fill="#444"></circle> {/each} </svg> </div>
问题现象
- 操作流程:点击一次「Add 2 Circles」按钮添加两个Circle实例,再点击两次「Delete Circle」按钮清空画布。此时Chrome堆内存中仍残留1个Circle实例未被回收。
- 累积效应:重复上述添加-删除循环,每次循环都会留存1个Circle实例,比如重复5次后,堆内存中会累积5个未被回收的实例。
- 差异表现:将
circles.shift()替换为pop()或slice()时,内存泄漏现象消失;使用SvelteMap替代$state([])的复杂场景中,也存在类似问题。
原因解析
这是Svelte响应式系统处理数组头部删除操作时的实现局限:
{#each}块的DOM复用逻辑:Svelte的{#each}会基于数组顺序复用DOM节点。当用shift()删除第一个元素时,数组元素整体前移,Svelte的diff算法在对比新旧数组差异时,可能会保留对被移除元素的引用,导致GC无法回收该实例。- 原地修改与新数组的差异:
shift()是原地修改数组,而slice()返回新数组会触发{#each}块完全重新渲染,旧数组元素能被正确回收;pop()删除末尾元素时,Svelte的diff逻辑更简单,可直接释放对应元素的引用。 - SvelteMap的类似逻辑:删除
SvelteMap中较早添加的键值对时,同样可能因diff跟踪机制未及时释放旧值引用,引发内存泄漏。
临时解决方案
- 替换
shift()为返回新数组的slice(1),触发完全渲染以释放旧实例:function deleteCircle() { circles = circles.slice(1); } - 针对
SvelteMap,可通过重新创建实例的方式触发完全更新,避免引用残留(需权衡性能损耗)。
内容的提问来源于stack exchange,提问作者chris838
相关产品推荐
相关产品推荐

