Edge-Native A/B Testing with Streaming HTML Rewriting

The Client-Side vs. Server-Side Experimentation Dilemma

A/B testing is a critical tool for driving data-backed optimization, but traditional client-side and server-side integration methodologies present clear trade-offs. Client-side testing scripts execute in the browser after the initial document paints, modifying elements dynamically. This delays the render process and frequently triggers a "flash of original content" (FOOC), causing severe Cumulative Layout Shift (CLS) penalties and hurting Core Web Vitals.

Server-side testing resolves layout shifts by generating variant-specific HTML on the server. However, it forces the CDN to bypass caching completely, routing every user request back to the database or rendering origin, which inflates global latency. Edge-native testing resolves this conflict by intercepting requests at the CDN edge and parsing the HTML stream on the fly.

How Streaming HTML Rewriters Work

Streaming HTML rewriting manipulates the HTML document chunk by chunk as it travels from the origin cache to the client browser. Rather than loading the entire HTML document into memory (which increases Time-to-First-Byte and consumes high RAM), a streaming parser matches specific selectors as bytes flow through, swapping classes, rewriting text, or injecting custom DOM elements.

This occurs in transit at the edge node, meaning the browser receives the modified HTML immediately. There is no layout shift, no client-side flicker, and no server-side compilation latency. The page loads as if it were a static, pre-rendered asset.

Bucketing, Cookies, and Cache Optimization

An edge-level A/B testing workflow contains three major architectural steps:

  • Cookie Bucketing: The edge worker parses the request headers. If an experimentation cookie is missing, the client is randomly bucketed into a group (e.g., 50% Control, 50% Variant-B) and a persistent Set-Cookie header is appended to the response.
  • Cache Partitioning: Custom cache keys are configured by appending the experiment bucket ID to the URL (e.g., https://example.com/checkout?bucket=variant-b). This prevents cache pollution and keeps the cache hit ratio high.
  • Streaming Transforms: The worker passes the response body through a specialized rewriter object that replaces specific HTML tags based on the assigned bucket.

Technical Implementation: Edge HTMLRewriter API Code

The following Cloudflare Worker implementation uses the HTMLRewriter API to rewrite a Call-to-Action (CTA) link text and styling based on a cookie-based experiment bucket:

// Class to handle element transformations during streaming
class HeroButtonRewriter {
  constructor(bucket) {
    this.bucket = bucket;
  }

  element(element) {
    if (this.bucket === 'variant-b') {
      // Modify classes and text content for Variant B
      element.setAttribute('class', 'cta-btn bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg');
      element.setInnerContent('Get Instant Access Now');
    } else {
      // Modify classes and text content for Control
      element.setAttribute('class', 'cta-btn bg-slate-900 hover:bg-slate-800 text-white font-bold py-3 px-8 rounded-lg shadow-md');
      element.setInnerContent('Start Free Trial');
    }
  }
}

export default {
  async fetch(request, env) {
    const response = await fetch(request);
    const cookieHeader = request.headers.get('Cookie') || '';
    
    let bucket = '';
    if (cookieHeader.includes('ab-experiment=variant-b')) {
      bucket = 'variant-b';
    } else if (cookieHeader.includes('ab-experiment=control')) {
      bucket = 'control';
    }
    else {
      // If new session, randomly assign control or variant-b
      bucket = Math.random() < 0.5 ? 'control' : 'variant-b';
    }

    // Initialize HTMLRewriter and target the main CTA button selector
    const rewriter = new HTMLRewriter()
      .on('a#landing-cta-btn', new HeroButtonRewriter(bucket));

    const transformedResponse = rewriter.transform(response);

    // Append cookie persistence header if it was a new assignment
    if (!cookieHeader.includes('ab-experiment=')) {
      const headers = new Headers(transformedResponse.headers);
      headers.append(
        'Set-Cookie',
        `ab-experiment=${bucket}; Path=/; Max-Age=1728000; Secure; SameSite=Lax`
      );
      
      return new Response(transformedResponse.body, {
        status: transformedResponse.status,
        headers: headers
      });
    }

    return transformedResponse;
  }
};

Flicker-Free Edge Experimentation with Bramsley

Our solutions guarantee that your experiments run with zero layout shifts, preserving 100/100 Core Web Vitals. Reach out to Bramsley today to supercharge your experimentation strategy at the network edge.

Bramsley Digital Studio

Enterprise Digital Architecture

We engineer digital infrastructure that drives measurable B2B growth. Experts in Legacy System Migration and High-Performance Frontends.

Architecture Specs & Case Studies

Scale Your Operations

  • Legacy System Migration
  • Scalable Infrastructure
  • High-Performance Frontends
  • Global Edge Deployment