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

Svelte拖拽网格与添加元素时过度重计算的优化问询

解决Svelte中不必要的重计算问题

我正在实现一款应用,用户可点击2D网格放置元素,也能拖拽网格移动。功能原型代码如下:

<script>
    let project = {
        offsetX: 0,
        offsetY: 0,
        circles: [{
            id: 1,
            x: 50,
            y: 50,
        }]
    }
    let dragInfo = null

    function onBackgroundDragStart(event){
        dragInfo = {
            action: "moveBackground",
            dragStartProjectOffsetX: project.offsetX,
            dragStartProjectOffsetY: project.offsetY,
            startMouseX: event.clientX,
            startMouseY: event.clientY
        }

        const dragImage = document.createElement("img")
        dragImage.style.display = "none"
        event.dataTransfer.setDragImage(dragImage, 0, 0)
    }

    function onBackgroundDragOver(event){
        switch(dragInfo.action){
            case "moveBackground":
                const dragDistanceX = event.clientX - dragInfo.startMouseX
                const dragDistanceY = event.clientY - dragInfo.startMouseY
                project.offsetX = dragInfo.dragStartProjectOffsetX + dragDistanceX
                project.offsetY = dragInfo.dragStartProjectOffsetY + dragDistanceY
            break
        }
    }

    // Create new circle on click.
    function onClick(event){
        project.circles.push({
            id: project.circles.length + 1,
            x: event.clientX - project.offsetX,
            y: event.clientY - project.offsetY,
        })
        project.circles = project.circles
    }

    function getText(circle){
        console.log("getText() is called.")
        return circle.x.toString()[0]
    }
</script>

<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
    class="app"
    draggable="true"
    on:dragstart={onBackgroundDragStart}
    on:dragover|preventDefault={onBackgroundDragOver}
    on:click={onClick}
    style:background-position-x={`${project.offsetX}px`}
    style:background-position-y={`${project.offsetY}px`}
>
    {#each project.circles as circle (circle.id)}
        <div
            class="circle"
            style:transform={`translate(${project.offsetX + circle.x}px, ${project.offsetY + circle.y}px) translate(-50%, -50%)`}
        >
            {getText(circle)}
        </div>
    {/each}
</div>

<style>
.app{
    position: relative;
    width: 100vw;
    height: 100vw;
    background-color: silver;
    background-image: linear-gradient(rgba(0, 0, 0, .1) .1em, transparent .1em), linear-gradient(90deg, rgba(0, 0, 0, .1) .1em, transparent .1em);
    background-size: 2em 2em;
    overflow: hidden;
}

.circle{
    position: absolute;
    width: 50px;
    height: 50px;
    background-color: red;
    border-radius: 50%;
    text-align: center;
}
</style>

当前原型功能正常,但存在两个过度重计算问题:

  • 拖拽背景时,仅更新project.offsetX和project.offsetY,但{#each}循环中每个circle的getText(circle)都会被重新调用,而该函数仅依赖circle.x,与offset参数无关;
  • 添加新circle时,所有已有circle的getText(circle)也会被重新调用。

实际场景中getText()是计算成本极高的函数,过度调用会导致应用卡顿。由于需要将项目信息整体保存为JSON,不想拆分project对象,请问如何让Svelte避免不必要的更新?


解决方案

1. 组件隔离状态依赖(最推荐)

将单个circle的渲染逻辑抽离成独立组件,组件内部只绑定自身依赖的状态,这样只有当circle本身变化时才会触发内部的getText计算,offset变化或新增其他circle时不会影响已有组件。

创建Circle.svelte组件:

<script>
    export let circle;
    export let offsetX;
    export let offsetY;

    function getText(circle){
        console.log("getText() is called.")
        return circle.x.toString()[0]
    }
</script>

<div
    class="circle"
    style:transform={`translate(${offsetX + circle.x}px, ${offsetY + circle.y}px) translate(-50%, -50%)`}
>
    {getText(circle)}
</div>

<style>
.circle{
    position: absolute;
    width: 50px;
    height: 50px;
    background-color: red;
    border-radius: 50%;
    text-align: center;
}
</style>

主组件中替换{#each}部分:

{#each project.circles as circle (circle.id)}
    <Circle {circle} offsetX={project.offsetX} offsetY={project.offsetY} />
{/each}

效果:

  • 拖拽背景时,offset变化只会触发每个Circle组件的样式更新,不会调用getText;
  • 新增circle时,只有新创建的Circle组件会调用getText,已有组件不会重复计算。

2. 预计算并缓存结果

如果不想拆组件,可以在添加circle时就预计算好文本内容,存储在circle对象中,后续直接使用缓存值,彻底避免重复计算。

修改onClick函数:

function onClick(event){
    const newX = event.clientX - project.offsetX
    const newCircle = {
        id: project.circles.length + 1,
        x: newX,
        y: event.clientY - project.offsetY,
        text: getText({x: newX}) // 预计算并缓存
    }
    project.circles.push(newCircle)
    project.circles = project.circles
}

模板中直接使用缓存值:

{#each project.circles as circle (circle.id)}
    <div
        class="circle"
        style:transform={`translate(${project.offsetX + circle.x}px, ${project.offsetY + circle.y}px) translate(-50%, -50%)`}
    >
        {circle.text}
    </div>
{/each}

注意:如果后续circle的x值可能修改,需要在修改时同步更新text字段。

3. 结合反应式语句与缓存函数

如果getText需要根据circle的动态变化重新计算,但又要避免不必要的调用,可以用缓存对象结合Svelte的反应式语句,仅在circle属性变化时更新计算结果。

主组件中添加缓存逻辑:

const textCache = new Map()

function getText(circle){
    if(textCache.has(circle.id)){
        return textCache.get(circle.id)
    }
    // 实际场景中的高成本计算
    const result = circle.x.toString()[0]
    textCache.set(circle.id, result)
    return result
}

// 监听circle的x变化,仅当x改变时更新缓存
$: project.circles.forEach(circle => {
    const currentResult = circle.x.toString()[0]
    if(textCache.get(circle.id) !== currentResult){
        textCache.set(circle.id, currentResult)
    }
})

模板中仍使用{getText(circle)},此时只有当circle的x变化时才会重新计算,offset变化或新增circle不会触发已有circle的getText调用(新增circle会调用一次)。


内容的提问来源于stack exchange,提问作者Peppe L-G

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 14:50:19