Skip to content

Conversation

@rafbcampos
Copy link
Contributor

@rafbcampos rafbcampos commented Aug 4, 2025

RFC: Fluent DSL Architecture for Player-UI Content Authoring

Executive Summary

This RFC proposes a fundamental architectural evolution for Player-UI content authoring through the adoption of a fluent DSL (Domain Specific Language) approach.

The current React-based DSL compilation performance limitations present critical barriers to scaling dynamic content generation (Player-UI content generation at runtime).

Our benchmarking demonstrates that a function-based fluent DSL approach could deliver 31-63x performance improvements over the current React implementation while maintaining full TypeScript support, IDE autocompletion, and developer experience benefits. This architectural shift addresses immediate performance bottlenecks while establishing a foundation for multi-language authoring, WebAssembly execution, and edge computing capabilities.

1. Problem Statement

1.1 Current Performance Crisis

The Player-UI ecosystem faces a critical performance bottleneck in DSL compilation:

  • Compilation Time: ~400ms for complex content (728KB JSON + 350KB user data)
  • CPU Usage: ~200% on M3 Pro 36GB during compilation
  • Service Capacity: Limited to 5 RPS on 7-unit 6GB K8s pod
  • Resource Constraints: CPU exhaustion causing API gateway timeouts (5s threshold)

1.2 Root Cause Analysis

Our performance profiling identified these key bottlenecks:

  1. React Reconciliation Overhead: Full Virtual DOM reconciliation for every update
  2. Hook System Processing: State management and lifecycle method execution
  3. Bundle size and framework tax: Using a UI rendering library for structured data generation

2. Current Architecture Limitations

2.1 Content Authoring Approaches

Player-UI currently supports two methods for content authoring:

JSON Authoring

  • Direct authoring in Player-UI's transport layer
  • Lightweight and fast to process
  • Limited developer tooling support
  • Prone to errors due to lack of type-checking
  • Redundancy (lack of content reusability)

React DSL

  • Rich developer tooling (type checking, syntax highlighting)
  • Component reusability and familiar development patterns
  • React hooks and state management
  • Higher computational overhead and framework-specific coupling

The React approach relies on react-json-reconciler, which waits for all useEffect and setState updates to settle before returning results, constrained by both React APIs and JS runtime limitations.

3. Proposed Solution: Fluent DSL Architecture

3.1 Fluent API Builders (Function-Based)

We propose a fundamental shift: replacing the React-based DSL with a dependency-free function-based approach for content that needs to be compiled/generated at runtime. This isn't a minor optimization — it's rethinking how we generate Player-UI content from first principles.

The function-based alternative offers:

  • Equal developer experience: Full TypeScript support with IDE autocompletion, inline documentation, and type checking
  • Superior performance: 31-63x faster content generation with half the CPU utilization
  • Framework independence: Zero external dependencies, eliminating React reconciliation overhead, and getting a smaller bundle (e.g. from 30mb to 300kb)
  • Functional composition: Natural building blocks that compose through standard functional patterns

3.2 Fluent API in Action

The fluent DSL provides a chainable, type-safe API for constructing Player-UI assets:

import {
  info,
  flow,
  text,
  action,
  important,
  subtitle,
} from "assets";
import { expression as e, binding as b } from "@player-tools/fluent";

const myTitle = text()
  .withTemplate`Hello ${important("World")} and ${subtitle("Universe")}`;

const welcomeView = info()
  .withId("view-1")
  .withTitle(myTitle)
  .withSubtitle(text().withValue("Getting Started Guide"))
  .withPrimaryInfo(
    text().withValue("This guide will help you get started with our service."),
  )
  .withActions([
    action()
      .withLabel(text().withValue("Continue"))
      .withExpression(e`value = "something"`),
  ]);

const result = flow({
  id: "test-flow",
  views: [welcomeView],
  data: {
    greeting: "Hello",
  },
  navigation: {
    BEGIN: "FLOW_1",
    FLOW_1: {
      startState: "VIEW_1",
      VIEW_1: {
        state_type: "VIEW",
        ref: "view-1",
        transitions: {},
      },
    },
  },
});

4. Performance Analysis

4.1 Benchmark Results

Our benchmarking across multiple approaches reveals dramatic performance improvements:

Performance Metrics (Average Execution Time in ms)

Content Size Functional DSL React DSL Improvement
Small 0.031 0.963 31x faster
Medium 0.074 3.573 48x faster
Large 0.136 8.638 63x faster

Alternative Approaches Comparison:

Approach Small Medium Large vs React
Functional DSL 0.031 0.074 0.136 31-63x faster
Custom JSX Runtime 0.128 0.299 0.539 7-16x faster
SolidJS 0.129 0.321 0.600 7-14x faster
React DSL 0.963 3.573 8.638 Baseline

4.2 Flame Graph Analysis

Performance profiling of complex content with a 300KB payload revealed these hotspots in the current React implementation:

  • createElementWithValidation (BoundLabeledText): 4%
  • coerceRef in updateContextProvider: 2.5%
  • reconcileChildren: 2.3%
  • reconcileChildrenArray: 1.7%
  • flatMap (flattenNode): 1.7%
  • createFiberFromElement: 1.4%
  • validateChildKeys (Slot): 1.1%

5. Technical Architecture

5.1 Core Components

Context-Aware ID Generation
Builders automatically generate hierarchical IDs based on parent context:

const form = collection()
  .withLabel(text().withValue("User Form")) // ID: "collection.label"
  .withValues([
    input().withBinding("user.name"), // ID: "collection.values-0"
    input().withBinding("user.email"), // ID: "collection.values-1"
  ]);

Tagged Template Integration
Support for dynamic content through tagged templates:

import { expression as e, binding as b } from "@player-tools/fluent";

const dynamicAction = action()
  .withValue(b`next`)
  .withLabel(text().withValue(e`submit(${b`formName`})`));

5. Advanced Features

5.1 Schema-Driven Development

Accepting bindings and expressions as strings exposes users to typos, trying to access nonexistent data, or using wrong arguments on expressions.

Generating bindings from the schema helps prevent that and adds nice IDE support with autocompletion based on the schema:

import { extractBindingsFromSchema } from "./extract-bindings-from-schema";
import { and, greaterThan, equal } from "./std";

const userSchema = {
  ROOT: {
    user: { type: "UserType" },
    settings: { type: "SettingsType" },
  },
  UserType: {
    name: { type: "StringType" },
    age: { type: "NumberType" },
    role: { type: "StringType" },
  },
  SettingsType: {
    minAge: { type: "NumberType" },
    adminRole: { type: "StringType" },
  },
} as const satisfies Schema.Schema;

const data = extractBindingsFromSchema(userSchema);

// Create complex type-safe expressions
const isAuthorizedAdmin = and(
  greaterThan(data.user.age, data.settings.minAge),
  equal(data.user.role, data.settings.adminRole),
);

console.log(isAuthorizedAdmin.toString());
// "{{data.user.age > data.settings.minAge && data.user.role == data.settings.adminRole}}"

5.2 Multi-Language, Edge Computing Future

The dependency-free function-based approach opens doors that React-based generation could never unlock:

Content Authoring Logic in Other Languages
Being dependency-free allows using different languages to create tooling to author/generate Player-UI content (e.g., Rust implementation).

WebAssembly Transformation
Without React and DOM dependencies, we can author and generate content leveraging WASM and more performant languages like Rust:

  • Execute content generation anywhere: Browser, server, edge, or mobile
  • Achieve near-native performance: WASM executes at close to native speeds
  • Edge computing capabilities: Content generation could move from centralized services to edge functions

6. Conclusion

The dependency-free function-based approach to Player-UI content authoring isn't just an optimization, it's aligning our tools with our true purpose. By freeing ourselves from framework dependencies, we gain performance, clarity, and future flexibility.

We are currently using the fluent pattern at Intuit, demonstrating that it's possible. The performance gains are real and substantial. The architectural benefits extend far beyond immediate performance improvements, opening doors to multi-language authoring, edge computing, and WebAssembly execution.

This RFC represents a fundamental change in how we think about Player-UI content authoring. Rather than accepting the compromises inherent in repurposing a general UI framework, we're establishing a purpose-built foundation that can evolve with our needs rather than being constrained by external framework decisions.

📦 Published PR as canary version: 0.13.0--canary.221.5964

Try this version out locally by upgrading relevant packages to 0.13.0--canary.221.5964

@rafbcampos rafbcampos self-assigned this Aug 4, 2025
@rafbcampos rafbcampos added the minor Increment the minor version when merged label Aug 4, 2025
@codecov
Copy link

codecov bot commented Aug 4, 2025

Codecov Report

❌ Patch coverage is 77.01613% with 570 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.66%. Comparing base (129d3b8) to head (837d81b).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...anguage/dsl/fluent/src/core/tagged-template/std.ts 61.88% 93 Missing ⚠️
language/dsl/fluent/src/core/utils/index.ts 0.00% 86 Missing ⚠️
...nguage/dsl/fluent/src/core/base-builder/context.ts 40.81% 58 Missing ⚠️
.../src/core/base-builder/resolution/path-resolver.ts 37.50% 49 Missing and 1 partial ⚠️
...src/core/base-builder/resolution/value-resolver.ts 77.34% 41 Missing ⚠️
...e/dsl/fluent/src/core/base-builder/id/generator.ts 78.78% 35 Missing ⚠️
...re/base-builder/resolution/steps/asset-wrappers.ts 34.14% 27 Missing ⚠️
...core/base-builder/resolution/steps/mixed-arrays.ts 60.60% 26 Missing ⚠️
...anguage/dsl/fluent/src/core/base-builder/errors.ts 34.48% 19 Missing ⚠️
...luent/src/core/base-builder/fluent-builder-base.ts 85.60% 18 Missing ⚠️
... and 17 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #221      +/-   ##
==========================================
+ Coverage   64.28%   65.66%   +1.37%     
==========================================
  Files         128      182      +54     
  Lines       13006    17519    +4513     
  Branches     1867     2621     +754     
==========================================
+ Hits         8361    11503    +3142     
- Misses       4617     5986    +1369     
- Partials       28       30       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rafbcampos rafbcampos force-pushed the fluent branch 2 times, most recently from fd1cb6f to f6953be Compare August 15, 2025 19:53
@rafbcampos rafbcampos marked this pull request as ready for review August 15, 2025 19:57
@rafbcampos rafbcampos requested a review from a team as a code owner August 15, 2025 19:57
@KetanReddy
Copy link
Member

Working through reviewing the details, but at the top I wanted to start the conversation around what we should actually call this. IMO fluent doesn't really convey what it is/does. Not sure I have a better idea at the moment but wanted to start getting the ball rolling on that.

@@ -0,0 +1,7 @@
export * from "./action";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be shipping examples of these components like this. IMO this should either just be:

  • In the docs
  • An unpublished package
  • Exported along side our existing reference assets from the player repo

Copy link
Contributor Author

@rafbcampos rafbcampos Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript lacks built-in documentation testing, making it easy for examples in external documentation to become stale and broken. By keeping examples in language/fluent/src/examples/ we ensure they're part of the regular test suite and CI pipeline, preventing drift between documentation and actual API behavior. This approach guarantees that examples remain functional and up-to-date, while documentation can safely link to these verified examples rather than maintaining separate, potentially outdated code snippets. Additionally, this method does not affect the bundle size, as we do not expose these examples in the index.ts file.

@rafbcampos
Copy link
Contributor Author

Working through reviewing the details, but at the top I wanted to start the conversation around what we should actually call this. IMO fluent doesn't really convey what it is/does. Not sure I have a better idea at the moment but wanted to start getting the ball rolling on that.

You're right that fluent doesn't explicitly describe the package's functionality, but I'd argue that's not necessarily a requirement for a good package name. Many packages use names that don't directly convey their purpose - Rust, Java, Liquid, and Player-UI are all examples where the name either became meaningful through adoption or references something tangential to the core functionality.

In our case, fluent specifically references the fluent API pattern we use throughout the builders - the method chaining approach that ensures objects are constructed in a valid state. So while it doesn't describe what the package does, it does describe how it works, which felt like a meaningful connection.

That said, I'm definitely open to alternatives if we can find something that feels more intuitive while still being concise. Do you have any initial thoughts on what direction might work better?

@rafbcampos rafbcampos marked this pull request as draft October 24, 2025 20:43
@rafbcampos rafbcampos force-pushed the fluent branch 6 times, most recently from 79582d3 to 0684fa9 Compare October 27, 2025 19:48
@rafbcampos rafbcampos marked this pull request as ready for review October 27, 2025 20:19
@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos rafbcampos force-pushed the fluent branch 2 times, most recently from 884ae8d to 0083505 Compare January 13, 2026 17:08
@rafbcampos
Copy link
Contributor Author

/canary

1 similar comment
@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

@rafbcampos
Copy link
Contributor Author

/canary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Increment the minor version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants