Skip to main content

Size Optimization for JS13K

The Lab13 SDK is designed for ultra-compact JS13K games. This guide covers techniques to minimize your game size while using the SDK effectively.

SDK Size Characteristics

Tree-Shakable Design

The SDK is built for maximum tree-shaking:

// ✅ Only these functions will be included in your bundle
import { useOnline, useMyId, useState } from 'lab13-sdk'

// ❌ Avoid importing unused functions
import * as Lab13 from 'lab13-sdk' // Imports everything!

Function Composition

No classes or inheritance - pure functions for maximum compression:

// ✅ Good: Function composition
const { getMyId } = useMyId()
const { getState } = useState()

// ❌ Avoid: Classes (not used in SDK)

Import Optimization

Selective Imports

Only import what you need:

// ✅ Minimal imports
import { useOnline, useMyId } from 'lab13-sdk'

// ✅ Add more as needed
import { useState, onClientJoined } from 'lab13-sdk'

// ❌ Avoid bulk imports
import * as Lab13 from 'lab13-sdk'

Import Patterns by Game Type

Simple Connection Only:

import { useOnline, useMyId } from 'lab13-sdk'

Basic Multiplayer:

import { useOnline, useMyId, useState, onClientJoined, onClientLeft } from 'lab13-sdk'

Advanced Multiplayer:

import {
useOnline,
useMyId,
useState,
createPositionNormalizer,
generateUUID,
ENTITY_COLLECTION_PREFIX,
} from 'lab13-sdk'

State Management Optimization

Minimal State Structure

Keep your state types as simple as possible:

// ✅ Good: Simple, flat structure
type PlayerState = {
x: number
y: number
s: number // score (short name)
}

// ❌ Avoid: Complex nested structures
type PlayerState = {
position: { x: number; y: number }
statistics: { score: number; level: number }
inventory: { items: string[] }
}

Use Short Property Names

Shorter names = smaller JSON payloads:

// ✅ Good: Short names
type PlayerState = {
x: number // position.x
y: number // position.y
s: number // score
n: string // name
}

// ❌ Avoid: Long names
type PlayerState = {
positionX: number
positionY: number
playerScore: number
playerName: string
}

Avoid Redundant Data

Don't store data that can be computed:

// ✅ Good: Store only essential data
type PlayerState = {
x: number
y: number
s: number
}

// ❌ Avoid: Storing computed values
type PlayerState = {
x: number
y: number
s: number
distanceFromOrigin: number // Can be computed
isAtCenter: boolean // Can be computed
}

Network Optimization

Use Normalizers

Normalize data to reduce payload size:

import { createPositionNormalizer } from 'lab13-sdk'

// Round positions to integers (saves ~50% size)
const normalizePosition = createPositionNormalizer(0)

const { updateMyState } = useState({
onBeforeSendDelta: (delta) => normalizePosition(delta),
})

Throttle Updates

Limit update frequency to reduce network traffic:

const { updateMyState } = useState({
deltaThrottleMs: 100, // 10 updates per second max
})

Send Only Changes

Only update what actually changed:

// ✅ Good: Only send changed data
updateMyState({
x: newX,
y: newY,
})

// ❌ Avoid: Sending unchanged data
updateMyState({
x: newX,
y: newY,
score: currentScore, // Don't send if unchanged
name: currentName, // Don't send if unchanged
})

Code Optimization

Minimize Function Calls

Reduce function call overhead:

// ✅ Good: Direct property access
const players = getPlayerStates()
const myState = players[getMyId()]

// ❌ Avoid: Multiple function calls
const myState = getPlayerStates()[getMyId()]
const myX = myState?.x || 0
const myY = myState?.y || 0

Use Short Variable Names

Shorter names compress better:

// ✅ Good: Short, clear names
const p = getPlayerStates()
const m = p[getMyId()]
const x = m?.x || 0

// ❌ Avoid: Long variable names
const playerStates = getPlayerStates()
const myPlayerState = playerStates[getMyId()]
const playerPositionX = myPlayerState?.x || 0

Inline Simple Logic

Avoid unnecessary functions for simple operations:

// ✅ Good: Inline simple logic
updateMyState({ x: keys['ArrowLeft'] ? x - 5 : x + 5 })

// ❌ Avoid: Separate function for simple logic
function updatePosition() {
if (keys['ArrowLeft']) {
updateMyState({ x: x - 5 })
} else {
updateMyState({ x: x + 5 })
}
}

Bundle Optimization

Use Tree Shaking

Ensure your bundler can tree-shake effectively:

// ✅ Good: Named imports
import { useOnline, useMyId } from 'lab13-sdk'

// ❌ Avoid: Default imports
import Lab13 from 'lab13-sdk'

Minimize Dependencies

Only use essential dependencies:

// ✅ Good: Minimal dependencies
import { useOnline } from 'lab13-sdk'
// No other dependencies needed

// ❌ Avoid: Heavy dependencies
import { useOnline } from 'lab13-sdk'
import lodash from 'lodash' // Too heavy for JS13K

Game-Specific Optimizations

Simple Multiplayer Games

For basic multiplayer (like chat, presence):

// Minimal setup - ~200 bytes
import { useOnline, useMyId } from 'lab13-sdk'
useOnline('room')
const { getMyId } = useMyId()

Movement-Based Games

For games with player movement:

// Movement setup - ~500 bytes
import { useOnline, useMyId, useState, createPositionNormalizer } from 'lab13-sdk'

useOnline('room')
const { getMyId } = useMyId()
const normalize = createPositionNormalizer(0)
const { updateMyState } = useState({ onBeforeSendDelta: normalize })

Entity-Based Games

For games with multiple entity types:

// Entity setup - ~800 bytes
import {
useOnline,
useMyId,
useState,
createPositionNormalizer,
generateUUID,
ENTITY_COLLECTION_PREFIX,
} from 'lab13-sdk'

useOnline('room')
const { getMyId } = useMyId()
const normalize = createPositionNormalizer(0)
const { updateMyState, updateState } = useState({ onBeforeSendDelta: normalize })

Size Monitoring

Track Bundle Size

Monitor your bundle size during development:

# Check bundle size
npx l13 build --debug

# Look for size information in output

Analyze Imports

Check what's being included:

// Add this temporarily to see what's imported
console.log('SDK functions:', { useOnline, useMyId, useState })

Common Pitfalls

1. Over-Engineering

Keep it simple:

// ✅ Good: Simple and direct
updateMyState({ x: newX, y: newY })

// ❌ Avoid: Over-engineered
const movementSystem = {
updatePosition: (x, y) => updateMyState({ x, y }),
}
movementSystem.updatePosition(newX, newY)

2. Unnecessary Abstractions

Avoid abstractions that add size:

// ✅ Good: Direct state updates
updateMyState({ score: score + 10 })

// ❌ Avoid: Unnecessary wrapper
function addScore(points) {
updateMyState({ score: getCurrentScore() + points })
}
addScore(10)

3. Redundant State

Don't store what you can compute:

// ✅ Good: Store only essential data
type PlayerState = { x: number; y: number; s: number }

// ❌ Avoid: Storing computed values
type PlayerState = {
x: number
y: number
s: number
distance: number // Can be computed from x,y
}

Size Budget Breakdown

For a 13KB game, consider this rough budget:

  • SDK Core: ~2-3KB (useOnline, useMyId, useState)
  • Game Logic: ~5-7KB
  • Assets: ~2-3KB
  • HTML/CSS: ~1KB

Total: ~10-14KB

Quick Size Checklist

  • Use selective imports only
  • Minimize state structure
  • Use short property names
  • Normalize data with normalizers
  • Throttle frequent updates
  • Send only changed data
  • Use short variable names
  • Inline simple logic
  • Avoid unnecessary abstractions
  • Monitor bundle size regularly

Example: Optimized Game

Here's an example of a size-optimized multiplayer game:

import { useOnline, useMyId, useState, createPositionNormalizer } from 'lab13-sdk'

// Connect
useOnline('game')

// Setup
const { getMyId } = useMyId()
const n = createPositionNormalizer(0)
const { updateMyState, getPlayerStates } = useState({ onBeforeSendDelta: n })

// Game loop
function loop() {
const p = getPlayerStates()
const m = p[getMyId()]
if (m) {
const x = m.x || 0
const y = m.y || 0
updateMyState({
x: keys['ArrowLeft'] ? x - 5 : keys['ArrowRight'] ? x + 5 : x,
y: keys['ArrowUp'] ? y - 5 : keys['ArrowDown'] ? y + 5 : y,
})
}
requestAnimationFrame(loop)
}

loop()

This approach keeps your game under the 13KB limit while providing full multiplayer functionality.