You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 13:26:20