📊 AoSoA
⚡ Ultra-lightweight data layout converter: Array of Structures ↔ Structure of Arrays.
Built for performance-critical applications on Edge, Serverless, and modern runtimes like Cloudflare, Deno, Vercel, Bun.
Zero dependencies. Zero overhead. Just pure TypeScript.
⚡ Why AoSoA?
- ✅ 0 dependencies — no extra weight
- ✅ 0 runtime overhead — pure JavaScript
- ✅ 0 config — import and go
- ✅ ~3KB gzipped — minimal footprint (8.50 KB minified)
- ✅ Type-safe — full TypeScript support
✨ Features
- 🔄 Bidirectional conversion — AoS ↔ SoA in both directions
- � Streaming API — Memory-efficient processing for large datasets
- 🗜️ Compression suite — RLE, Delta, Dictionary encoding (5-10x savings)
- 🏊 Memory pooling — TypedArrayPool to reduce GC pressure
- 🛠️ SoA manipulation — Filter, map, slice, concat, and update operations
- �🎯 Column filtering — select only the columns you need
- 🔧 Flexible options — shallow/deep copy, custom fill values, key sorting
- 📊 Performance optimized — efficient memory layout for data-intensive tasks
- ⚡ Performance metrics — Built-in measurement with detailed breakdowns
- 🛡️ Input validation — Automatic type checking with helpful error messages
- 🔍 Strict type detection — Optional validation for data consistency
- 🧩 Works everywhere:
- ✅ Cloudflare Workers
- ✅ Deno Deploy
- ✅ Bun
- ✅ Node.js (16+)
- ✅ Modern Browsers
- 💡 Fully tree-shakable
- 📦 Rich type utilities — 15+ TypeScript utility types included
- 🎮 TypedArray support — GPU-ready buffers for WebGL/WebGPU
📦 Install
npm install aosoa
📚 What is AoS and SoA?
Array of Structures (AoS)
Traditional object array where each element is a complete record:
const users = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
]
Structure of Arrays (SoA)
Column-oriented layout where each property becomes an array:
const users = {
id: [1, 2],
name: ['Alice', 'Bob'],
age: [25, 30]
}
Why SoA? Better cache locality, SIMD-friendly, efficient for large datasets and batch operations.
🎯 When to Use AoSoA
⚠️ Important: Most JavaScript applications don't need AoSoA. Modern JavaScript engines (V8, SpiderMonkey) optimize AoS (Array of Structures) very well, and JSON/AoS is more ergonomic for typical web development.
✨ Use AoSoA when you're working with:
🎮 WebGL / WebGPU Graphics
SoA with TypedArrays enables direct GPU buffer uploads:
import { toSoaTyped } from 'aosoa'
const particles = Array.from({ length: 100_000 }, () => ({
x: Math.random() * 100,
y: Math.random() * 100,
z: Math.random() * 100,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
vz: (Math.random() - 0.5) * 2,
life: Math.random(),
size: Math.random() * 2
}))
const soa = toSoaTyped(particles, { arrayType: Float32Array })
gl.bufferData(gl.ARRAY_BUFFER, soa.x, gl.STATIC_DRAW)
gl.bufferData(gl.ARRAY_BUFFER, soa.y, gl.STATIC_DRAW)
gl.bufferData(gl.ARRAY_BUFFER, soa.z, gl.STATIC_DRAW)
🕹️ ECS / Game Simulations
Cache-friendly memory layout for component systems:
import { toSoa } from 'aosoa'
const entities = Array.from({ length: 50_000 }, (_, i) => ({
posX: Math.random() * 1000,
posY: Math.random() * 1000,
rotation: Math.random() * Math.PI * 2,
velX: (Math.random() - 0.5) * 10,
velY: (Math.random() - 0.5) * 10,
mass: Math.random() * 10 + 1,
}))
const soa = toSoa(entities)
for (let i = 0; i < soa.posX.length; i++) {
soa.posX[i] += soa.velX[i] * deltaTime
soa.posY[i] += soa.velY[i] * deltaTime
}
📊 ML / Numerical Processing
Column-based operations for data science:
import { toSoa } from 'aosoa'
const measurements = Array.from({ length: 100_000 }, () => ({
temperature: Math.random() * 100 - 20,
humidity: Math.random() * 100,
pressure: Math.random() * 50 + 950
}))
const soa = toSoa(measurements)
const avgTemp = soa.temperature.reduce((a, b) => a + b) / soa.temperature.length
const minTemp = Math.min(...soa.temperature)
const maxTemp = Math.max(...soa.temperature)
const features = { temp: soa.temperature, humid: soa.humidity }
💾 WASM / Binary I/O
Continuous memory for WebAssembly interop:
import { toSoaTyped } from 'aosoa'
const data = Array.from({ length: 10_000 }, (_, i) => ({
x: i * 0.1,
y: Math.sin(i * 0.1),
z: Math.cos(i * 0.1)
}))
const soa = toSoaTyped(data)
const xPtr = wasmModule.exports.allocate(soa.x.byteLength)
const xBuffer = new Uint8Array(wasmModule.exports.memory.buffer, xPtr, soa.x.byteLength)
xBuffer.set(new Uint8Array(soa.x.buffer))
await Bun.write('data.bin', Buffer.from(soa.x.buffer))
📈 Large-Scale Visualization
Efficient column scanning for charts/graphs:
import { toSoa } from 'aosoa'
const timeseries = loadLargeDataset()
const soa = toSoa(timeseries, {
columns: ['timestamp', 'value']
})
const chartData = {
x: soa.timestamp,
y: soa.value,
type: 'scatter',
mode: 'lines'
}
const filtered = soa.value.filter((v, i) =>
soa.timestamp[i] > startTime && soa.timestamp[i] < endTime
)
🔧 CSV / Columnar Data Processing
Natural fit for column-oriented formats:
import { toSoa, toAos } from 'aosoa'
const csvData = parseCSV(file)
const soa = toSoa(csvData)
const statistics = {
ids: soa.id,
uniqueNames: [...new Set(soa.name)],
avgAge: soa.age.reduce((a, b) => a + b) / soa.age.length
}
const result = toAos(soa)
📊 Performance Benchmarks
Real-world performance comparisons:
| 10k particles | Position update | 3.2ms | 1.1ms | 2.9x |
| 100k records | Column sum | 8.5ms | 2.8ms | 3.0x |
| 1M points | Filter by value | 125ms | 45ms | 2.8x |
| WebGL upload | Buffer prep | 15ms | 2ms | 7.5x |
Test Environment: M1 MacBook Pro with Bun 1.3.0
Why faster?
- ✅ Better CPU cache utilization (sequential memory access)
- ✅ SIMD optimization opportunities (V8 auto-vectorization)
- ✅ Zero object allocation in hot loops
- ✅ TypedArray direct memory access (no boxing)
🔧 API Usage
🔄 Convert AoS to SoA
import { toSoa } from 'aosoa'
const aos = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
]
const soa = toSoa(aos)
🎮 Convert to TypedArray SoA (WebGL/GPU)
import { toSoaTyped } from 'aosoa'
const vertices = [
{ x: 0.0, y: 0.5, z: 0.0 },
{ x: -0.5, y: -0.5, z: 0.0 },
{ x: 0.5, y: -0.5, z: 0.0 }
]
const soa = toSoaTyped(vertices)
gl.bufferData(gl.ARRAY_BUFFER, soa.x, gl.STATIC_DRAW)
🔄 Convert SoA to AoS
import { toAos } from 'aosoa'
const soa = {
id: [1, 2],
name: ['Alice', 'Bob'],
age: [25, 30]
}
const aos = toAos(soa)
🎯 Column Filtering
import { toSoa } from 'aosoa'
const users = [
{ id: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
{ id: 2, name: 'Bob', age: 30, email: 'bob@example.com' }
]
const soa = toSoa(users, { columns: ['id', 'name'] })
🔧 Handle Missing Values
import { toSoa } from 'aosoa'
const data = [
{ id: 1, name: 'Alice' },
{ id: 2 }
]
const soa = toSoa(data, { fill: 'unknown' })
📊 Get Data Statistics
import { getDataStats } from 'aosoa'
const aos = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
]
const stats = getDataStats(aos)
⚡ Performance Measurement (NEW!)
Track conversion performance with built-in metrics:
import { toSoa } from 'aosoa'
const data = Array.from({ length: 100_000 }, (_, i) => ({
id: i,
x: Math.random(),
y: Math.random()
}))
const result = toSoa(data, { measure: true })
console.log(result.metrics)
const soa = result.data
🛡️ Type Safety with Validation (NEW!)
Automatic input validation with helpful error messages:
import { toSoa, toSoaTyped } from 'aosoa'
try {
toSoa({ id: 1, name: 'Alice' })
} catch (error) {
console.error(error.message)
}
const mixedData = [
{ id: 1, value: 100 },
{ id: 2, value: '200' },
{ id: 3, value: 300 }
]
try {
toSoaTyped(mixedData, { strictTypeDetection: true })
} catch (error) {
console.error(error.message)
}
const soa = toSoaTyped(mixedData)
📖 API Reference
toSoa<T>(aos, options?)
Converts Array of Structures to Structure of Arrays.
Parameters:
aos (AoS<T>): Array of objects to convert
options (ToSoaOptions<T>, optional):
columns (keyof T[]): Select specific columns. Default: all keys
fill (any): Value for missing properties. Default: undefined
sortKeys (boolean): Sort keys alphabetically. Default: false
shallow (boolean): Use shallow copy. Default: true
measure (boolean): NEW! Return performance metrics. Default: false
Returns: SoA<T> or ConversionWithMetrics<SoA<T>> - Structure of Arrays (with metrics if measure=true)
toSoaTyped<T>(aos, options?)
Converts Array of Structures to Structure of Arrays with TypedArrays for numerical columns.
Parameters:
aos (AoS<T>): Array of objects to convert
options (ToSoaTypedOptions<T>, optional):
arrayType (TypedArrayConstructor): Default TypedArray type. Default: Float32Array
columnTypes (Partial<Record<keyof T, TypedArrayConstructor>>): Per-column TypedArray types
strictTypeDetection (boolean): NEW! Validate type consistency. Default: false
measure (boolean): NEW! Return performance metrics. Default: false
Returns: SoATyped<T> or ConversionWithMetrics<SoATyped<T>> - Structure of Arrays with TypedArrays
Throws: TypeError if input is invalid or types are inconsistent (with strictTypeDetection=true)
Example:
const soa = toSoaTyped(particles, {
arrayType: Float32Array,
columnTypes: {
id: Uint32Array,
life: Float64Array
}
})
toAos<T>(soa, options?)
Converts Structure of Arrays to Array of Structures.
Parameters:
soa (SoA<T>): Structure of Arrays to convert
options (ToAosOptions<T>, optional):
columns (keyof T[]): Select specific columns. Default: all keys
length (number): Fixed output length. Default: longest array
measure (boolean): NEW! Return performance metrics. Default: false
Returns: AoS<T> or ConversionWithMetrics<AoS<T>> - Array of Structures
Throws: TypeError if input is not a valid SoA
getDataStats(data)
Analyzes data structure and returns statistics.
Parameters:
data (AoS<T> | SoA<T>): Data to analyze
Returns: DataStats - Data statistics
rowCount (number): Number of rows
columnCount (number): Number of columns
columns (string[]): Column names
isAoS (boolean): Whether data is AoS format
🎨 Type Utilities
AoSoA includes rich TypeScript utilities:
import type {
AoS,
SoA,
SoATyped,
TypedArray,
TypedArrayConstructor,
ColumnKey,
ColumnArray,
ToSoaOptions,
ToSoaTypedOptions,
ToAosOptions,
DataStats,
Particle,
Vec3
} from 'aosoa'
📘 Full Example
import { toSoa, toAos, type AoS, type SoA } from 'aosoa'
interface User {
id: number
name: string
email: string
age: number
}
const users: AoS<User> = [
{ id: 1, name: 'Alice', email: 'alice@example.com', age: 25 },
{ id: 2, name: 'Bob', email: 'bob@example.com', age: 30 },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', age: 35 }
]
const usersSoa: SoA<User> = toSoa(users)
const averageAge = usersSoa.age.reduce((a, b) => a + b, 0) / usersSoa.age.length
const processedUsers = toAos(usersSoa)
console.log(users === processedUsers)
🧪 Tests & Demo
- ✅ 118 test cases covering all features
- ✅ Tested with Bun Test
- ✅ Interactive demo available:
npm run demo
Explore AoS ↔ SoA conversions interactively with:
- Basic conversions
- Advanced options (filtering, sorting, fill values)
- Performance benchmarks (50,000+ records)
- Round-trip integrity checks
🏃 Performance
Benchmarks on M1 MacBook Pro:
| 10k records | AoS → SoA | ~8ms |
| 10k records | SoA → AoS | ~6ms |
| 100k records | AoS → SoA | ~47ms |
| 100k records | Round-trip | ~90ms |
Results may vary based on data complexity and hardware.
📦 Built With
- 📐 Pure TypeScript — no external dependencies
- ⚡ Bun for testing and development
- 🏗️ Dual build: TypeScript declarations + minified bundle
- 🧪 Comprehensive test coverage (118 tests)
🤝 Contributing
Contributions welcome! Please feel free to submit a Pull Request.
📄 License
MIT
🔗 Related
💖 Made with ❤️ for performance-critical JavaScript applications