Optimizing Performance
This page will outline a few scenarios where you may be able to optimize performance of your application. Svelte and Pixi are very performant so you won’t have to think about it too much but they still have their limits!
Rendering Lots of Components
You may find yourself needing to render and update many components at once. Take the app on the homepage, for example.
If we were to recreate this, we might imagine every Star as a <Sprite /> component that receives props every frame.
<script> import * as PIXI from 'pixi.js' import { Sprite, onTick } from 'svelte-pixi'
const width = 720 const height = 400 const speed = 0.025 const fov = 20 const starSize = 0.05
let cameraZ = 0 let amount = $state(100)
// create an array describing each star's initial position let stars = $derived( new Array(amount).fill(null).map(() => { const deg = Math.random() * Math.PI * 2 const distance = Math.random() * 50 + 1
return { x: Math.cos(deg) * distance, y: Math.sin(deg) * distance, z: Math.random() * 1000 + 750, } }) )
// move the camera forward onTick((ticker) => { cameraZ += ticker.deltaTime * 10 * speed })</script>
{#each stars as star} <!-- calculate new position based on cameraZ --> {@const z = star.z - cameraZ} {@const distance = Math.max(0, (2000 - z) / 2000)}
<Sprite texture={PIXI.Texture.from('/assets/star.png')} anchor={{ x: 0.5, y: 0.7 }} scale={{ x: distance * starSize, y: distance * starSize }} x={star.x * (fov / z) * width + width / 2} y={star.y * (fov / z) * height + height / 2} />{/each}
<label class="flex flex-col !mt-0"> <span class="text-white text-center">Amount: {amount}</span> <input type="range" min="0" max="10000" step="100" bind:value={amount} /></label><script> import * as PIXI from 'pixi.js' import { Sprite, onTick } from 'svelte-pixi/svelte-4'
const width = 720 const height = 400 const speed = 0.025 const fov = 20 const starSize = 0.05
let cameraZ = 0 let amount = 100
// create an array describing each star's initial position $: stars = new Array(amount).fill(null).map(() => { const deg = Math.random() * Math.PI * 2 const distance = Math.random() * 50 + 1
return { x: Math.cos(deg) * distance, y: Math.sin(deg) * distance, z: Math.random() * 1000 + 750, } })
// move the camera forward onTick((ticker) => { cameraZ += ticker.deltaTime * 10 * speed })</script>
{#each stars as star} <!-- calculate new position based on cameraZ --> {@const z = star.z - cameraZ} {@const distance = Math.max(0, (2000 - z) / 2000)}
<Sprite texture={PIXI.Texture.from('/assets/star.png')} anchor={{ x: 0.5, y: 0.7 }} scale={{ x: distance * starSize, y: distance * starSize }} x={star.x * (fov / z) * width + width / 2} y={star.y * (fov / z) * height + height / 2} />{/each}
<label class="flex flex-col !mt-0"> <span class="text-white text-center">Amount: {amount}</span> <input type="range" min="0" max="10000" step="100" bind:value={amount} /></label>Notice how the performance drops as you increase the amount of stars with the slider. It is important to remember that mounting, unmounting, and updating props of components always come at a cost, but this is only really noticed when you are rendering thousands of components.
When working at this scale every optimization counts, so it is best to create Pixi objects directly.
<script>import * as PIXI from 'pixi.js'import { onTick, Container } from 'svelte-pixi'
const width = 720const height = 400const speed = 0.025const fov = 20const starSize = 0.05
let container = $state()let cameraZ = 0let amount = $state(5000)
let stars = $derived.by(() => { container.removeChildren()
return new Array(amount).fill(null).map(() => { // we're going to manually create our Sprite instances this time const star = new PIXI.Sprite(PIXI.Texture.from('/assets/star.png'))
const deg = Math.random() * Math.PI * 2 const distance = Math.random() * 50 + 1
// these are custom values that we'll use in the updateStar function star.initX = Math.cos(deg) * distance star.initY = Math.sin(deg) * distance star.initZ = Math.random() * 1000 + 750
star.x = star.initX star.y = star.initY star.z = star.initZ
updateStar(star) container.addChild(star)
return star })})
// instead of updating these values as props we'll mutate them on the star instancesfunction updateStar(star) { const z = star.z - cameraZ const distance = Math.max(0, (2000 - z) / 2000)
star.scale.set(distance * starSize) star.anchor.set(0.5, 0.7)
star.x = star.initX * (fov / z) * width + width / 2 star.y = star.initY * (fov / z) * height + height / 2}
// move the camera forwardonTick((ticker) => { cameraZ += ticker.deltaTime * 10 * speed
for (const star of stars) { if (!star.destroyed) { updateStar(star) } }})</script>
<Container bind:instance={container} />
<label class="flex flex-col !mt-0"><span class="text-white text-center">Amount: {amount}</span><input type="range" min="0" max="10000" step="100" bind:value={amount} /></label><script> import * as PIXI from 'pixi.js' import { onTick, Container } from 'svelte-pixi/svelte-4' import { onMount } from 'svelte'
const width = 720 const height = 400 const speed = 0.025 const fov = 20 const starSize = 0.05
let container let cameraZ = 0 let amount = 5000 let stars = []
// create stars for amount value $: { // create stars if (container) { container.removeChildren() stars = new Array(amount).fill(null).map(() => { // we're going to manually create our Sprite instances this time const star = new PIXI.Sprite(PIXI.Texture.from('/assets/star.png'))
const deg = Math.random() * Math.PI * 2 const distance = Math.random() * 50 + 1
// these are custom values that we'll use in the updateStar function star.initX = Math.cos(deg) * distance star.initY = Math.sin(deg) * distance star.initZ = Math.random() * 1000 + 750
star.x = star.initX star.y = star.initY star.z = star.initZ
updateStar(star) container.addChild(star)
return star }) } }
// instead of updating these values as props we'll mutate them on the star instances function updateStar(star) { const z = star.z - cameraZ const distance = Math.max(0, (2000 - z) / 2000)
star.scale.set(distance * starSize) star.anchor.set(0.5, 0.7)
star.x = star.initX * (fov / z) * width + width / 2 star.y = star.initY * (fov / z) * height + height / 2 }
// move the camera forward onTick((ticker) => { cameraZ += ticker.deltaTime * 10 * speed
for (const star of stars) { updateStar(star) } })
onMount(() => { // since we created the stars manually we'll // need to destroy stars on unmount as well return () => { for (const star of stars) { star.destroy() } } })</script>
<Container bind:instance={container} />
<label class="flex flex-col !mt-0"> <span class="text-white text-center">Amount: {amount}</span> <input type="range" min="0" max="10000" step="100" bind:value={amount} /></label>Performance is much better now and there’s hardly any stutter when adding/removing stars.
If you wanted to squeeze out a bit more you could use a ParticleContainer instead of a regular Container.
Render on Demand
Pixi applications typically render at 60 frames per second (or higher if the user’s screen has a higher refresh rate). This is perfectly fine and most WebGL apps function this way, but it could be wasteful to keep rendering if everything in your scene has stopped moving or animating. In which case it would be better to only render when our components have actually updated (e.g. after user interaction).
You can set render="demand" on the Application component to opt into this behaviour:
<script> import { onMount } from 'svelte' import { Text, Application } from 'svelte-pixi' import DraggableCircle from '$lib/components/DraggableCircle.svelte'</script>
<Application width={400} height={400} antialias render="demand" onrender={() => console.log('render')}> <DraggableCircle x={200} y={200} /> <Text x={200} y={300} text="Click and drag" style={{ fill: 'white' }} anchor={0.5} /></Application><script> import { onMount } from 'svelte' import { Text, Application } from 'svelte-pixi/svelte-4' import DraggableCircle from '$lib/components/DraggableCircle.svelte'</script>
<Application width={400} height={400} antialias render="demand" on:render={() => console.log('render')}> <DraggableCircle x={200} y={200} /> <Text x={200} y={300} text="Click and drag" style={{ fill: 'white' }} anchor={0.5} /></Application>If you are using the Renderer component, see this example.
Triggering Renders Manually
Every SveltePixi component will automatically trigger renders when they are updated when rendering on demand. If you are mutating Pixi instances directly or adding functionality to a base component you will need to mark that an update is required manually.
<script> import { Sprite, getRenderer, onTick } from 'svelte-pixi'
// invalidate tells the renderer to render on the next tick const { invalidate } = getRenderer()
let sprite let moving = true
// we can still use the ticker! onTick((ticker) => { if (sprite && moving) { sprite.x += 5 * ticker.deltaTime
if (sprite.x > 200) { moving = false }
invalidate() } })</script>
<Sprite bind:instance={sprite} /><script> import { Sprite, getRenderer } from 'svelte-pixi'
let { customProp, customProp2, ...restProps } = $props() const { invalidate } = getRenderer()
class CustomSprite extends PIXI.Sprite { customProp = "1" customProp2 = "2" }
let sprite = new CustomSprite()
// apply our custom props $effect(() => { sprite.customProp = customProp invalidate() })
$effect(() => { sprite.customProp2 = customProp2 invalidate() })</script>
<Sprite instance={sprite} {...restProps} /><script> import { Sprite, getRenderer, onTick } from 'svelte-pixi/svelte-4'
// invalidate tells the renderer to render on the next tick const { invalidate } = getRenderer()
let sprite let moving = true
// we can still use the ticker! onTick((ticker) => { if (sprite && moving) { sprite.x += 5 * ticker.deltaTime
if (sprite.x > 200) { moving = false }
invalidate() } })</script>
<Sprite bind:instance={sprite} /><script> import { Sprite, getRenderer } from 'svelte-pixi'
export let customProp export let customProp2
const { invalidate } = getRenderer()
class CustomSprite extends PIXI.Sprite { customProp = "1" customProp2 = "2" }
let sprite = new CustomSprite()
// apply our custom props $: { sprite.customProp = customProp invalidate() }
$: { sprite.customProp2 = customProp2 invalidate() }</script>
<Sprite instance={sprite} {...$$restProps} />