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'
  import FPS from '../_/FPS.svelte'

  const width = 400
  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(delta => {
    cameraZ += delta * 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) * width + height / 2}
  />
{/each}

<FPS />

<label>
  <span>Amount: {amount}</span>
  <input type="range" min="0" max="10000" step="100" bind:value={amount}/>
</label>
  

Notice how the performance sharply drops as you increase the amount of stars with the slider. Not great is it? There are a few reasons why:

  1. Updating props for thousands of components is slow. Even if these were HTML elements, it would be just as bad. At this scale it’s always best to mutate the underlying Pixi instances (in this case, the sprites).

  2. Mounting/unmounting thousands of components is slow. If you pay close attention you’ll notice a significant stutter when moving the slider up/down at the higher values.

Let’s take another approach by using Pixi a bit more directly:

<script>
  import * as PIXI from 'pixi.js'
  import { Sprite, onTick, Container } from 'svelte-pixi'
  import { onMount } from 'svelte'
  import FPS from '../_/FPS.svelte'

  const width = 400
  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
  $: {
    // destroy previous stars if amount was changed
    stars.forEach(star => star.destroy())

    // create stars
    if (container) {
      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)

        return star
      })

      if (stars.length) {
        container.addChild(...stars)
      }
    }
  }

  // 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) * width + height / 2
  }

  // move the camera forward
  onTick(delta => {
    cameraZ += delta * 10 * speed

    stars.forEach(updateStar)
  })

  onMount(() => {
    // since we created the stars manually we'll
    // need to destroy stars on unmount as well
    return () => {
      stars.forEach(star => star.destroy())
    }
  })
</script>

<FPS />

<Container bind:instance={container} />

<label>
  <span>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/site/DraggableCircle.svelte'
</script>

<Application
  width={400}
  height={400}
  antialias
  render="demand"
  on:postrender={() => console.log('render')}
>
  <Text
    x={200}
    y={300}
    text="Click and drag"
    style={{ fill: 'white' }}
    anchor={0.5}
  />
  <DraggableCircle x={200} y={200} />
</Application>
  

If you are using the Renderer component, see this example.

Triggering Renders Manually

Every Svelte Pixi 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'

  // gets the underlying Renderer component from Application
  const { invalidate } = getRenderer()

  let sprite
  let moving = true

  // we can still use the ticker!
  onTick((delta) => {
    if (sprite && moving) {
      sprite.x += 5 * delta

      if (sprite.x > 200) {
        moving = false
      }

      invalidate()
    }
  })

</script>

<Sprite bind:instance={sprite} {...$$restProps} />
<!-- CustomSprite.svelte -->
<script>
  import { Sprite, getRenderer, onTick } from 'svelte-pixi'
  import { afterUpdate } from 'svelte'

  // imagine we have a custom Sprite class that extends PIXI.Sprite
  import CustomSprite from '../CustomSprite.js'

  export let customProp
  export let customProp2

  const { invalidate } = getRenderer()

  let sprite = new CustomSprite()

  // apply our custom props
  $: sprite.customProp = customProp
  $: sprite.customProp2 = customProp2

  // svelte will run this whenever this component's props or state changes
  afterUpdate(() => {
    invalidate()
  })

</script>

<Sprite instance={sprite} {...$$restProps} />