Skip to content

perf(cli): add Effect.cached to preset loading for performance #35

@aridyckovsky

Description

@aridyckovsky

Summary

Use Effect.cached to memoize preset loading, avoiding redundant dynamic imports when the same preset is referenced multiple times.

Problem

If a user's config references the same preset multiple times (or if multiple commands load the same config), the preset is imported fresh each time:

// Current implementation
const loadPreset = (name: string): Effect.Effect<Preset, PresetLoadError> =>
  Effect.gen(function* () {
    const module = yield* Effect.tryPromise({
      try: () => import(name), // Fresh import every time
      catch: error => new PresetLoadError({ preset: name, message: String(error) })
    })
    // ...
  })

Inefficiency: Each call to loadPreset("@effect-migrate/preset-basic") re-imports the module, even though module loading is inherently cacheable.

Proposed Solution

Use Effect.cached to memoize preset loading per preset name:

import { Cache, Effect } from "effect"

// Create a cache with preset name as key
const presetCache = Cache.make({
  capacity: 100, // Max number of presets to cache
  timeToLive: "infinity", // Presets don't change during CLI run
  lookup: loadPresetUncached
})

const loadPresetUncached = (name: string): Effect.Effect<Preset, PresetLoadError> =>
  Effect.gen(function* () {
    const module = yield* Effect.tryPromise({
      try: () => import(name),
      catch: error => new PresetLoadError({ preset: name, message: String(error) })
    })
    
    const preset = module.default ?? module.preset
    
    if (!isValidPreset(preset)) {
      return yield* Effect.fail(
        new PresetLoadError({
          preset: name,
          message: "Invalid preset shape: must have 'rules' array"
        })
      )
    }
    
    return preset
  })

// Public API
export const loadPreset = (name: string): Effect.Effect<Preset, PresetLoadError> =>
  Effect.gen(function* () {
    const cache = yield* presetCache
    return yield* cache.get(name)
  })

Alternative: Simple Memoization

For simpler approach without Cache:

import { Effect } from "effect"

const loadPresetEffect = (name: string): Effect.Effect<Preset, PresetLoadError> =>
  Effect.gen(function* () {
    const module = yield* Effect.tryPromise({
      try: () => import(name),
      catch: error => new PresetLoadError({ preset: name, message: String(error) })
    })
    // ... validation
  })

// Memoize with Effect.cached
const cachedLoadPreset = Effect.cached(loadPresetEffect)

export const loadPreset = (name: string) => cachedLoadPreset(name)

Benefits

  • Performance: Avoid redundant imports
  • Consistency: Same preset instance used throughout CLI run
  • Memory efficient: Cache bounded by number of unique presets (small)

Use Cases

  1. Multiple commands: audit and metrics both load config → same preset imported twice
  2. Config reload: If config is reloaded during watch mode
  3. Testing: Faster test runs when same preset used across tests

Acceptance Criteria

  • Preset loading memoized per name
  • Cache bounded (max 100 presets)
  • First load imports module, subsequent loads use cache
  • All existing tests pass
  • Performance test added (verify cache hits)

Measurement

Before/after benchmark:

it.effect("should cache preset loads", () =>
  Effect.gen(function* () {
    const start1 = Date.now()
    yield* loadPreset("@effect-migrate/preset-basic")
    const duration1 = Date.now() - start1
    
    const start2 = Date.now()
    yield* loadPreset("@effect-migrate/preset-basic") // Should be cached
    const duration2 = Date.now() - start2
    
    expect(duration2).toBeLessThan(duration1 * 0.1) // 10x faster
  })
)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    effect-tsEffect-TS patterns and usagelow-hanging-fruitEasy wins and quick improvementspkg:cliIssues related to @effect-migrate/cli packagepriority:lowLow prioritytype:featureNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions