npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mango0422/micro-charts

v0.2.4

Published

Lightweight, zero-dependency chart library inspired by uPlot. Canvas-based, high performance.

Readme

Micro Charts

Lightweight alternative to recharts with uPlot-inspired optimizations. Zero dependencies. Canvas-based. 11 chart types.

npm version bundle size

Features

  • Lightweight: ~60KB (IIFE), ~18KB gzipped - 10x smaller than recharts
  • Zero dependencies: Completely standalone, no external libraries
  • Fast: Canvas-based rendering with uPlot-inspired optimizations
    • Style caching (minimal Canvas API calls)
    • Global RAF scheduler (efficient batched rendering)
    • Zero-allocation loops (reduced GC pressure)
  • Tree-shakeable: Import only what you need, individual charts 1-3KB
  • 11 Chart Types: Gauge, Pie, Bar, Progress, StackedBar, HeatMap, Radar, Funnel, HorizontalBar, VerticalBar, MultiLine
  • Framework-agnostic: Works with React, Vue, Svelte, or Vanilla JS
  • TypeScript: Full type definitions included

When to Use

Choose the right tool for your use case:

| Feature | Micro Charts | uPlot | recharts | | -------------------- | ---------------- | ----- | -------- | | Bundle Size | ~60KB | ~50KB | ~500KB | | Dependencies | 0 | 0 | 10+ | | Chart Types | 11 | 1 | 20+ | | Time-series | ✅ | ✅✅✅ | ✅ | | Dashboards/KPIs | ✅✅✅ | ❌ | ✅✅ | | Large Datasets | Good (10K pts) | Excellent (1M+ pts) | Fair (1K pts) | | Animations | ✅ | ❌ | ✅ | | React Native | Manual | Manual | Native | | Learning Curve | Easy | Medium | Easy |

Use Micro Charts when:

  • ✅ Building dashboards with diverse chart types (Gauge, Pie, Radar, etc.)
  • ✅ Replacing recharts to reduce bundle size by 90%
  • ✅ Need zero dependencies and full control
  • ✅ Working with moderate datasets (up to 10K points)

Use uPlot when:

  • ✅ Building high-performance time-series visualizations
  • ✅ Handling massive datasets (100K - 1M+ datapoints)
  • ✅ Need extreme optimization (60fps streaming at 10% CPU)

Use recharts when:

  • ✅ Deep React ecosystem integration required
  • ✅ Need extensive customization and components
  • ✅ Bundle size is not a concern

Installation

npm install @mango0422/micro-charts
# or
pnpm add @mango0422/micro-charts
# or
yarn add @mango0422/micro-charts

CDN (IIFE)

<!-- jsdelivr (recommended - faster) -->
<script src="https://cdn.jsdelivr.net/npm/@mango0422/micro-charts/dist/index.global.js"></script>

<!-- unpkg (alternative) -->
<!-- <script src="https://unpkg.com/@mango0422/micro-charts/dist/index.global.js"></script> -->

<script>
  const { GaugeChart } = MicroCharts;
</script>

Quick Start

GaugeChart

import { GaugeChart } from "@mango0422/micro-charts";

const gauge = new GaugeChart(document.getElementById("gauge"), {
  value: 75,
  max: 100,
  thresholds: { warning: 60, critical: 80 },
});

// Update value
gauge.setData(90);

// Cleanup
gauge.destroy();

PieChart

import { PieChart } from "@mango0422/micro-charts";

const pie = new PieChart(
  document.getElementById("pie"),
  [
    { label: "Normal", value: 120 },
    { label: "Warning", value: 30 },
    { label: "Error", value: 10 },
  ],
  { innerRadius: 0.5 } // Donut mode
);

BarChart

import { BarChart } from "@mango0422/micro-charts";

const bar = new BarChart(
  document.getElementById("bar"),
  [
    { label: "Server 1", value: 85 },
    { label: "Server 2", value: 60 },
    { label: "Server 3", value: 95 },
  ],
  { orientation: "horizontal" }
);

ProgressBar

import { ProgressBar } from "@mango0422/micro-charts";

const progress = new ProgressBar(document.getElementById("progress"), {
  value: 75,
  max: 100,
});

StackedBarChart

import { StackedBarChart } from "@mango0422/micro-charts";

const stackedBar = new StackedBarChart(
  document.getElementById("stacked"),
  {
    categories: ["Mon", "Tue", "Wed", "Thu", "Fri"],
    series: [
      { label: "Inbound", data: [120, 150, 180, 140, 200] },
      { label: "Outbound", data: [80, 90, 100, 95, 110] },
      { label: "Error", data: [5, 8, 3, 10, 6] },
    ],
  },
  { showTotal: true, showLegend: true }
);

HeatMap

import { HeatMap } from "@mango0422/micro-charts";

const heatmap = new HeatMap(
  document.getElementById("heatmap"),
  {
    rows: ["00:00", "01:00", "02:00", "03:00"],
    columns: ["Mon", "Tue", "Wed", "Thu", "Fri"],
    values: [
      [120, 150, 180, 140, 200],
      [80, 90, 100, 95, 110],
      [60, 70, 65, 80, 75],
      [40, 50, 55, 60, 65],
    ],
  },
  { colorScheme: "sequential", showValues: true }
);

RadarChart

import { RadarChart } from "@mango0422/micro-charts";

const radar = new RadarChart(
  document.getElementById("radar"),
  {
    axes: [
      { label: "CPU", max: 100 },
      { label: "Memory", max: 100 },
      { label: "Disk", max: 100 },
      { label: "Network", max: 100 },
    ],
    datasets: [
      { label: "Server A", data: [80, 70, 90, 60], color: "#3b82f6" },
      { label: "Server B", data: [60, 85, 75, 80], color: "#ef4444" },
    ],
  },
  { showGrid: true, showLegend: true }
);

FunnelChart

import { FunnelChart } from "@mango0422/micro-charts";

const funnel = new FunnelChart(
  document.getElementById("funnel"),
  [
    { label: "Packets Received", value: 10000 },
    { label: "Valid Packets", value: 9500 },
    { label: "Processed", value: 9200 },
    { label: "Forwarded", value: 8800 },
    { label: "Delivered", value: 8500 },
  ],
  { showPercentage: true, orientation: "vertical" }
);

HorizontalBarChart

import { HorizontalBarChart } from "@mango0422/micro-charts";

const hbar = new HorizontalBarChart(
  document.getElementById("hbar"),
  [
    { label: "Core 0", value: 45 },
    { label: "Core 1", value: 78 },
    { label: "Core 2", value: 92 },
  ],
  {
    domain: [0, 100],
    showGrid: true,
    tooltip: {
      formatter: (value) => `${value}% Usage`,
    },
  }
);

VerticalBarChart

import { VerticalBarChart } from "@mango0422/micro-charts";

const vbar = new VerticalBarChart(
  document.getElementById("vbar"),
  [
    { label: "0-10%", value: 12 },
    { label: "10-30%", value: 8 },
    { label: "30-50%", value: 4 },
    { label: "50-70%", value: 2 },
  ],
  {
    allowDecimals: false,
    showGrid: true,
  }
);

MultiLineChart

import { MultiLineChart } from "@mango0422/micro-charts";

const line = new MultiLineChart(
  document.getElementById("line"),
  [
    { timestamp: 1706800000000, eth0: 1500000, eth1: 2300000 },
    { timestamp: 1706800060000, eth0: 1620000, eth1: 2100000 },
    { timestamp: 1706800120000, eth0: 1480000, eth1: 2500000 },
  ],
  {
    series: [
      { key: "eth0", name: "Interface 0", color: "#3b82f6" },
      { key: "eth1", name: "Interface 1", color: "#22c55e" },
    ],
    yAxis: {
      tickFormatter: (v) => `${(v / 1000000).toFixed(1)} Mbps`,
    },
    tooltip: {
      filter: (value) => value !== null && value > 0,
      sort: (a, b) => b.value - a.value,
    },
  }
);

API Reference

GaugeChart

new GaugeChart(container: HTMLElement, options: GaugeChartOptions)

Options:

| Option | Type | Default | Description | | ------------ | ------- | --------- | ------------------------------- | | value | number | required | Current value | | min | number | 0 | Minimum value | | max | number | 100 | Maximum value | | size | number | 200 | Canvas size (px) | | thickness | number | 0.2 | Arc thickness ratio (0-1) | | thresholds | object | - | { warning: number, critical: number } | | colors | object | - | Custom colors for states | | showValue | boolean | true | Show value text | | animate | boolean | true | Enable animation | | duration | number | 500 | Animation duration (ms) |

Methods:

  • setData(value: number) - Update value with animation
  • setOptions(options: Partial<GaugeChartOptions>) - Update options
  • resize(size: number) - Resize the chart
  • destroy() - Cleanup and remove

PieChart

new PieChart(container: HTMLElement, data: PieChartData[], options?: PieChartOptions)

Data:

interface PieChartData {
  label: string;
  value: number;
  color?: string; // Auto-generated if not provided
}

Options:

| Option | Type | Default | Description | | ---------------- | -------- | ------- | ------------------------------ | | size | number | 200 | Canvas size (px) | | innerRadius | number | 0 | Inner radius ratio (0-1), 0 = pie, >0 = donut | | startAngle | number | -90 | Start angle (degrees) | | animate | boolean | true | Enable animation | | duration | number | 600 | Animation duration (ms) | | showLabels | boolean | false | Show segment labels | | onSegmentClick | function | - | Click handler (index, data) => void |

Methods:

  • setData(data: PieChartData[]) - Update data with animation
  • setOptions(options: Partial<PieChartOptions>) - Update options
  • resize(size: number) - Resize the chart
  • destroy() - Cleanup and remove

BarChart

new BarChart(container: HTMLElement, data: BarChartData[], options?: BarChartOptions)

Data:

interface BarChartData {
  label: string;
  value: number;
  color?: string;
}

Options:

| Option | Type | Default | Description | | -------------- | -------- | ---------- | ------------------------------ | | width | number | 400 | Canvas width (px) | | height | number | 300 | Canvas height (px) | | orientation | string | 'vertical' | 'vertical' or 'horizontal' | | max | number | auto | Maximum value (auto-calculated)| | barThickness | number | 0.6 | Bar thickness ratio (0-1) | | showValues | boolean | false | Show value labels | | showGrid | boolean | true | Show background grid | | animate | boolean | true | Enable animation | | duration | number | 500 | Animation duration (ms) |

Methods:

  • setData(data: BarChartData[]) - Update data with animation
  • setOptions(options: Partial<BarChartOptions>) - Update options
  • resize(width: number, height: number) - Resize the chart
  • destroy() - Cleanup and remove

ProgressBar

new ProgressBar(container: HTMLElement, options: ProgressBarOptions)

Options:

| Option | Type | Default | Description | | ----------------- | ------- | --------- | ------------------------ | | value | number | required | Current value | | max | number | 100 | Maximum value | | width | number | 300 | Width (px) | | height | number | 20 | Height (px) | | color | string | '#3b82f6' | Fill color | | backgroundColor | string | '#e5e7eb' | Background color | | showPercentage | boolean | true | Show percentage text | | animate | boolean | true | Enable animation | | duration | number | 400 | Animation duration (ms) | | borderRadius | number | 4 | Border radius (px) |

Methods:

  • setData(value: number) - Update value with animation
  • setOptions(options: Partial<ProgressBarOptions>) - Update options
  • resize(width: number, height: number) - Resize the bar
  • destroy() - Cleanup and remove

StackedBarChart

new StackedBarChart(container: HTMLElement, data: StackedBarData, options?: StackedBarOptions)

Data:

interface StackedBarData {
  categories: string[]; // X-axis labels
  series: Array<{
    label: string; // Series name
    data: number[]; // Values for each category
    color?: string; // Auto-generated if not provided
  }>;
}

Options:

| Option | Type | Default | Description | | -------------- | -------- | ---------- | ---------------------------------- | | width | number | 500 | Canvas width (px) | | height | number | 300 | Canvas height (px) | | orientation | string | 'vertical' | 'vertical' or 'horizontal' | | barThickness | number | 0.7 | Bar thickness ratio (0-1) | | showValues | boolean | false | Show value labels on segments | | showTotal | boolean | true | Show total on top of stack | | showLegend | boolean | true | Show series legend | | showGrid | boolean | true | Show background grid | | animate | boolean | true | Enable animation | | duration | number | 600 | Animation duration (ms) | | stacked100 | boolean | false | Normalize to 100% |

Methods:

  • setData(data: StackedBarData) - Update data with animation
  • setOptions(options: Partial<StackedBarOptions>) - Update options
  • resize(width: number, height: number) - Resize the chart
  • destroy() - Cleanup and remove

HeatMap

new HeatMap(container: HTMLElement, data: HeatMapData, options?: HeatMapOptions)

Data:

interface HeatMapData {
  rows: string[]; // Y-axis labels
  columns: string[]; // X-axis labels
  values: number[][]; // 2D array [row][col]
}

Options:

| Option | Type | Default | Description | | ---------------- | -------- | ------------ | -------------------------------------- | | width | number | 600 | Canvas width (px) | | height | number | 400 | Canvas height (px) | | cellPadding | number | 2 | Padding between cells | | showValues | boolean | false | Show numeric values in cells | | colorScheme | string | 'sequential' | 'sequential' or 'diverging' | | colors | object | - | { min, mid, max } color configuration| | min | number | auto | Manual min value | | max | number | auto | Manual max value | | showAxisLabels | boolean | true | Show row/column labels | | animate | boolean | true | Enable animation | | duration | number | 400 | Animation duration (ms) | | onCellClick | function | - | Click handler (row, col, value) => void|

Methods:

  • setData(data: HeatMapData) - Update data with animation
  • setOptions(options: Partial<HeatMapOptions>) - Update options
  • resize(width: number, height: number) - Resize the chart
  • destroy() - Cleanup and remove

RadarChart

new RadarChart(container: HTMLElement, data: RadarChartData, options?: RadarChartOptions)

Data:

interface RadarChartData {
  axes: Array<{
    label: string; // Axis name
    max?: number; // Max value for this axis
  }>;
  datasets: Array<{
    label: string; // Dataset name
    data: number[]; // Values for each axis
    color?: string; // Auto-generated if not provided
    fill?: boolean; // Fill polygon, default: true
    fillOpacity?: number; // Fill opacity 0-1, default: 0.2
  }>;
}

Options:

| Option | Type | Default | Description | | ------------ | ------- | ------- | ---------------------------- | | size | number | 400 | Canvas size (px, square) | | levels | number | 5 | Number of concentric levels | | showGrid | boolean | true | Show grid lines | | showLabels | boolean | true | Show axis labels | | showValues | boolean | false | Show data point values | | showLegend | boolean | true | Show dataset legend | | animate | boolean | true | Enable animation | | duration | number | 600 | Animation duration (ms) |

Methods:

  • setData(data: RadarChartData) - Update data with animation
  • setOptions(options: Partial<RadarChartOptions>) - Update options
  • resize(size: number) - Resize the chart
  • destroy() - Cleanup and remove

FunnelChart

new FunnelChart(container: HTMLElement, data: FunnelChartData, options?: FunnelChartOptions)

Data:

type FunnelChartData = Array<{
  label: string; // Stage name
  value: number; // Count at this stage
  color?: string; // Auto-generated if not provided
}>;

Options:

| Option | Type | Default | Description | | ---------------- | -------- | ---------- | ---------------------------------- | | width | number | 500 | Canvas width (px) | | height | number | 400 | Canvas height (px) | | orientation | string | 'vertical' | 'vertical' or 'horizontal' | | neckRatio | number | 0.3 | Width ratio at bottom | | gap | number | 4 | Gap between stages | | showLabels | boolean | true | Show stage labels | | showValues | boolean | true | Show values | | showPercentage | boolean | true | Show % of previous stage | | animate | boolean | true | Enable animation | | duration | number | 600 | Animation duration (ms) | | onStageClick | function | - | Click handler (index, stage) => void|

Methods:

  • setData(data: FunnelChartData) - Update data with animation
  • setOptions(options: Partial<FunnelChartOptions>) - Update options
  • resize(width: number, height: number) - Resize the chart
  • destroy() - Cleanup and remove

HorizontalBarChart

new HorizontalBarChart(container: HTMLElement, data: HorizontalBarData[], options?: HorizontalBarChartOptions)

Data:

interface HorizontalBarData {
  label: string;
  value: number;
  color?: string;
  metadata?: Record<string, unknown>;
}

Options:

| Option | Type | Default | Description | | ---------------- | -------- | --------- | -------------------------------------- | | width | number | 400 | Canvas width (px) | | height | number | auto | Auto-calculated based on bar count | | barHeight | number | 16 | Height of each bar | | barSpacing | number | 6 | Spacing between bars | | barRadius | number | 4 | Right corner radius | | domain | [number, number] | [0, 100] | Value range | | showGrid | boolean | true | Show vertical grid lines | | gridDash | number[] | [3, 3] | Grid line dash pattern | | labelWidth | number | 80 | Width reserved for labels | | showValues | boolean | true | Show value labels | | valueFormatter | function | - | Format value display | | tooltip | object/false | false | Tooltip configuration | | animate | boolean | true | Enable animation | | duration | number | 500 | Animation duration (ms) |

Methods:

  • setData(data: HorizontalBarData[]) - Update data with animation
  • setOptions(options: Partial<HorizontalBarChartOptions>) - Update options
  • resize(width?: number, height?: number) - Resize the chart
  • destroy() - Cleanup and remove

VerticalBarChart

new VerticalBarChart(container: HTMLElement, data: VerticalBarData[], options?: VerticalBarChartOptions)

Data:

interface VerticalBarData {
  label: string;
  value: number;
  color?: string;
}

Options:

| Option | Type | Default | Description | | ---------------- | -------- | ------------ | -------------------------------------- | | width | number | 400 | Canvas width (px) | | height | number | 300 | Canvas height (px) | | barThickness | number | 0.6 | Bar thickness ratio (0-1) | | barRadius | number | 4 | Top corner radius | | yDomain | [number, number] or 'auto' | 'auto' | Y-axis range | | yTickCount | number | 5 | Number of Y-axis ticks | | allowDecimals | boolean | false | Allow decimal values on Y-axis | | showGrid | boolean | true | Show horizontal grid lines | | gridDash | number[] | [3, 3] | Grid line dash pattern | | xAxisHeight | number | 30 | Space for X-axis labels | | tooltip | object/false | false | Tooltip configuration | | animate | boolean | true | Enable animation | | duration | number | 500 | Animation duration (ms) |

Methods:

  • setData(data: VerticalBarData[]) - Update data with animation
  • setOptions(options: Partial<VerticalBarChartOptions>) - Update options
  • resize(width?: number, height?: number) - Resize the chart
  • destroy() - Cleanup and remove

MultiLineChart

new MultiLineChart(container: HTMLElement, data: MultiLineData[], options: MultiLineChartOptions)

Data:

interface MultiLineData {
  timestamp: number; // Unix timestamp in milliseconds
  [key: string]: number | null; // Dynamic keys for each series
}

interface SeriesConfig {
  key: string; // Data key
  name: string; // Display name
  color: string; // Series color
  id?: string; // Optional unique identifier
}

Options:

| Option | Type | Default | Description | | -------------- | -------- | ------------ | -------------------------------------- | | width | number | 600 | Canvas width (px) | | height | number | 300 | Canvas height (px) | | margin | object | { top: 20, right: 20, bottom: 30, left: 70 } | Margins | | series | SeriesConfig[] | required | Series configuration | | lineWidth | number | 1.5 | Line width | | lineType | string | 'linear' | 'linear', 'monotone', or 'step' | | connectNulls | boolean | true | Connect null values | | xAxis | object | - | X-axis configuration | | yAxis | object | - | Y-axis configuration | | showGrid | boolean | true | Show horizontal grid lines | | gridDash | number[] | [3, 3] | Grid line dash pattern | | tooltip | object/false | false | Tooltip configuration | | animate | boolean | true | Enable animation | | duration | number | 500 | Animation duration (ms) |

Methods:

  • setData(data: MultiLineData[]) - Update data with animation
  • setSeries(series: SeriesConfig[]) - Update series configuration
  • setOptions(options: Partial<MultiLineChartOptions>) - Update options
  • resize(width?: number, height?: number) - Resize the chart
  • destroy() - Cleanup and remove

Framework Integration

React

import { useEffect, useRef } from "react";
import { GaugeChart } from "@mango0422/micro-charts";

function Gauge({ value }: { value: number }) {
  const ref = useRef<HTMLDivElement>(null);
  const chartRef = useRef<GaugeChart>();

  useEffect(() => {
    if (ref.current) {
      chartRef.current = new GaugeChart(ref.current, { value });
      return () => chartRef.current?.destroy();
    }
  }, []);

  useEffect(() => {
    chartRef.current?.setData(value);
  }, [value]);

  return <div ref={ref} />;
}

Vue 3

<template>
  <div ref="chartRef"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from "vue";
import { GaugeChart } from "@mango0422/micro-charts";

const props = defineProps<{ value: number }>();
const chartRef = ref<HTMLDivElement>();
let chart: GaugeChart;

onMounted(() => {
  if (chartRef.value) {
    chart = new GaugeChart(chartRef.value, { value: props.value });
  }
});

onUnmounted(() => {
  chart?.destroy();
});

watch(
  () => props.value,
  (newValue) => {
    chart?.setData(newValue);
  }
);
</script>

Performance

Benchmarks

Rendering performance on typical hardware (tested with various data sizes):

| Data Points | Multi-Line | Bar Charts | Pie/Gauge | |-------------|------------|------------|-----------| | 100 | <5ms | <3ms | <2ms | | 1,000 | <15ms | <8ms | <5ms | | 10,000 | <50ms | <30ms | <20ms | | 100,000 | <500ms | N/A* | N/A* |

*Bar and Pie charts aren't practical with 100K+ items

Bundle Size Comparison

| Library | Bundle Size | Dependencies | Chart Types | Best For | | -------------- | ----------- | ------------ | ----------- | -------- | | Micro Charts | ~60KB (18KB gzipped) | 0 | 11 | Dashboards, KPIs | | uPlot | ~50KB (15KB gzipped) | 0 | 1 (time-series) | Large time-series only | | Chart.js | ~200KB | 0 | 8 | General purpose | | recharts | ~500KB | 10+ | 20+ | React apps, rich features |

Performance Optimizations

  • Canvas Rendering: Hardware-accelerated, HiDPI-aware
  • Style Caching: Minimize Canvas API calls by caching fill/stroke styles
  • Global RAF Scheduler: Batch all animations and renders in single requestAnimationFrame loop
  • Zero-Allocation Loops: Avoid GC pressure with index-based iteration
  • Geometry Caching: Pre-calculate bar rectangles and layout values

When to Use What

Use Micro Charts if:

  • Building dashboards or monitoring UIs
  • Need variety (gauge, pie, bar, time-series)
  • Working with <10K data points per chart
  • Want minimal bundle size impact

Use uPlot if:

  • Only need time-series charts
  • Dealing with 100K+ data points
  • Performance is absolutely critical
  • Can sacrifice chart variety for speed

Use recharts if:

  • Already using React
  • Need extensive customization
  • Want rich interactions (zoom, brush, etc.)
  • Bundle size isn't a concern

Running Benchmarks

npm run build
open benchmark/index.html

See benchmark/README.md for details.

Development

# Install dependencies
pnpm install

# Build
pnpm build

# Watch mode
pnpm dev

Inspiration

Performance optimizations inspired by uPlot:

  • Canvas-based rendering
  • Style caching
  • Global RAF scheduler
  • Zero-allocation loops

License

MIT © mango0422