Skip to content

Add minimal browser bundle without SuperJSON dependency #24

@nickchomey

Description

@nickchomey

Note: this is all generated by Claude Sonnet 4.5. If there's any errors, so be it - the issue is real, and I think the ask is reasonable.

Problem Statement

Currently, @kunkun/kkrpc/browser includes SuperJSON and its dependency chain:

  • superjson (90KB)
  • copy-anything (9KB)
  • is-what (36KB)
  • Total: 135KB unminified → ~14KB minified

For many browser use cases, this overhead is unnecessary because:

  1. MessagePort/BroadcastChannel already support structured clone

    • Can handle: Objects, Arrays, Maps, Sets, Dates, typed arrays, ArrayBuffers
    • Native browser API, zero overhead
  2. SharedWorker/Worker communication doesn't need SuperJSON's advanced features

    • Class instance preservation
    • Circular reference handling
    • Symbol support
    • These are rarely needed in RPC scenarios
  3. Bundle size matters for browser apps

    • Every KB counts for mobile/slow networks, and low-end mobile devices
    • 14KB is significant when the core RPC logic is only 15KB. Compression gets it down to under 5kb, but the browser still has to parse it all.

Impact

Real-world example from our migration:

  • Before (official package): 29KB minified total-
  • After (custom build with superjson-stub): 15KB minified total
  • Size decrease: -48% solely from SuperJSON

Proposed Solutions

Solution 1: New export browser-minimal (RECOMMENDED)

Add a new subpath export that uses plain JSON:

// jsr.json
{
  "exports": {
    "./browser": "./browser-mod.ts",
    "./browser-minimal": "./browser-minimal-mod.ts"
  }
}
// browser-minimal-mod.ts
export * from "./src/adapters/worker.ts"
export * from "./src/adapters/iframe.ts"
export * from "./src/adapters/websocket.ts"
export * from "./src/adapters/tauri.ts"
export * from "./src/interface.ts"
export * from "./src/channel-minimal.ts"  // Uses JSON instead of superjson
export * from "./src/utils.ts"
// Note: serialization.ts excluded
// src/channel-minimal.ts
// Copy of channel.ts but with:
import { JSONSerializer } from "./serialization-json.ts"  // Plain JSON impl

Pros:

  • Zero breaking changes
  • Users opt-in via import path
  • Clear naming convention
  • ~15KB bundle size

Cons:

  • Code duplication (channel.ts vs channel-minimal.ts)
  • Needs separate tests

Solution 2: Make superjson optional

// src/channel.ts
import superjson from "superjson"  // Keep default behavior

export class RPCChannel<...> {
  constructor(
    io: IoInterface,
    options?: {
      serializer?: Serializer  // Allow injection
      // ... other options
    }
  ) {
    this.serializer = options?.serializer ?? superjson
  }
}

Pros:

  • Single implementation
  • Maximum flexibility
  • Users can BYO serializer

Cons:

  • Breaking change (need major version bump)
  • More complex API
  • Bundle includes superjson by default unless tree-shaken

Recommended Implementation

Solution 1 (new export) is cleanest:

  1. No breaking changes
  2. Clear opt-in model
  3. Minimal code changes
  4. Easy to document

Migration Path (if implemented)

// Before
import { RPCChannel } from '@kunkun/kkrpc/browser'

// After (for minimal bundle)
import { RPCChannel } from '@kunkun/kkrpc/browser-minimal'

Questions for Maintainers

  1. Are there use cases that absolutely require SuperJSON in browser contexts?

Workaround (for users today)

Users can import from kkrpc source with import maps:

{
  "imports": {
    "@kkrpc-source/": "https://raw.githubusercontent.com/kunkunsh/kkrpc/main/packages/kkrpc/src/",
    "superjson": "./superjson-stub.ts"
  }
}

superjson-stub.ts

/**
 * Minimal superjson stub to avoid the 135KB dependency chain.
 *
 * This replaces superjson when bundling from kkrpc source.
 * Works because our MessagePort transport already handles structured clone.
 */
export default {
  stringify: (obj: unknown) => JSON.stringify(obj),
  parse: (str: string) => JSON.parse(str),
  serialize: (obj: unknown) => ({ json: obj, meta: undefined }),
  deserialize: (payload: { json: unknown }) => payload.json,
};

This works but requires:

  • Manual version tracking
  • Import map configuration
  • Custom stub implementation

An official minimal export would be cleaner.


Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions