File size: 7,954 Bytes
24a2ddf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# Claude PDF Export Performance Analysis

## Problem: Performance Regression After CDN Localization

### Test Results

| Test ID | Time | Widget Render | Description |
|---------|------|---------------|-------------|
| 17:02:05 | 22,923ms | 17,063ms | **Before optimization** |
| 18:08:55 | 25,469ms | 19,612ms | After browser pool fix |
| 20:35:52 | 27,282ms | 19,410ms | **After CDN localization** |

**Result: CDN localization made performance WORSE (+2,347ms)**

## Root Cause Analysis

### What the CDN Interception Does

From backend logs:
```
[WIDGET] Chart.js CDN interception: ENABLED (local version: 200.3KB)
[WIDGET] Intercepting Chart.js request: https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js
[WIDGET] Chart.js served from local file (200.3KB)
[WIDGET-PERF]   setContent + networkidle0: 1957ms
[WIDGET-PERF]   render: 1105ms
```

### Why It Made Things Worse

#### 1. Browser Request Interception Has Overhead

The code uses `BrowserContext.setServerInterception()` which:
- Adds overhead per network request (interception check + response handling)
- Each widget makes multiple requests (HTML + Chart.js + other resources)
- Total: 15 widgets Γ— multiple requests Γ— interception overhead

#### 2. Chart.js CDN Already Fast

Cloudflare CDN caches Chart.js aggressively:
- Chart.js (200KB) likely loads in <100ms from CDN
- Local file serving has similar or worse overhead (filesystem I/O + interception handling)

#### 3. Real Bottleneck is Chart.js EXECUTION

Backend timing breakdown per widget:
```
setContent + networkidle0: 600-2000ms  (includes CDN + interception)
render: 2000-2500ms                    ← CHART.JS EXECUTION IS THE BOTTLENECK
screenshot: 40-220ms
```

The `render` phase (2-2.5 seconds) is where Chart.js:
- Parses the JavaScript code
- Initializes the Chart object
- Computes chart layout
- Draws bars/lines/pies to canvas
- Applies animations and styling

**CDN localization only saves the download time, not the execution time.**

### Concurrency Analysis

With MAX_CONCURRENT=3 and 15 widgets:
```
Batch 1: widgets 0-2   (~4 seconds)
Batch 2: widgets 3-5   (~4 seconds)
Batch 3: widgets 6-8   (~4 seconds)
Batch 4: widgets 9-11  (~4 seconds)
Batch 5: widgets 12-14 (~4 seconds)
Total: ~20 seconds
```

This matches the actual test time (19.4 seconds), confirming the bottleneck is per-widget rendering time, not network.

## Why CDN Localization Failed

### Initial Hypothesis (Wrong)

```
CDN download (2-3s) β†’ Widget render (1s)
Local file (0s) + Widget render (1s)
Expected savings: 2-3s per widget
```

### Actual Behavior (Reality)

```
CDN download (0.1s) + interception overhead (0.2s) + render (2.5s) = 2.8s
Local file (0.1s) + interception overhead (0.2s) + render (2.5s) = 2.8s
Savings: 0s, but added interception overhead = WORSE
```

Cloudflare CDN already caches Chart.js efficiently, so the download time was already minimal.

### What the Interception Actually Does

From server.js:
```typescript
// BrowserContext-level request interception
await browserContext.setServerInterception({
  urlPattern: '**/Chart.js/**/*.js',
  handler: async (route) => {
    const chartJsContent = fs.readFileSync('/app/lib/chart.umd.js', 'utf8');
    await route.fulfill({
      status: 200,
      contentType: 'application/javascript',
      body: chartJsContent
    });
  }
});
```

The interception handler:
1. Catches the Chart.js request
2. Reads local file (filesystem I/O)
3. Returns response to browser

Each step adds latency, and with 15 widgets making these requests, the overhead compounds.

## Performance Bottleneck Breakdown

### Current Bottleneck Distribution

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Widget Render Time: ~4 seconds per widget               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ setContent + networkidle:  600-2000ms  (network + DOM)  β”‚
β”‚ Chart.js DOWNLOAD:            100-200ms  (already fast) β”‚
β”‚ Chart.js EXECUTION:          2000-2500ms  ← BOTTLENECK  β”‚
β”‚ Screenshot:                    40-220ms                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

### Where the Time Goes

1. **Chart.js Initialization** (~500ms)
   - Parse JavaScript code
   - Set up Chart namespace and utilities
   - Configure defaults and helpers

2. **Chart Computation** (~1000ms)
   - Calculate scales and axes
   - Compute bar/line/pie positions
   - Apply data transformations

3. **Canvas Drawing** (~1000ms)
   - Draw grid lines and labels
   - Render bars/lines/pies
   - Apply colors and gradients
   - Draw legends and tooltips

## What Doesn't Work

### ❌ CDN Localization

**Reason**: Cloudflare CDN already fast; interception overhead negates benefit

### ❌ Increasing MAX_CONCURRENT

**Reason**: Each widget already takes CPU time; more concurrency = more CPU contention

### ❌ Disabling Chart.js Animations

**Reason**: Already disabled in current implementation

## What Could Work (Future Optimization)

### Option 1: Chart.js Worker Pool

Pre-render charts in web workers to avoid blocking:
- Complex to implement
- Limited benefit (still need to wait for rendering)

### Option 2: Chart.js Caching

Cache rendered chart images to avoid re-rendering identical charts:
- Cache key: (chart type, data JSON, dimensions)
- Benefit: Repeated charts render instantly
- Drawback: First render still slow

### Option 3: Use Lightweight Chart Libraries

Replace Chart.js with lighter alternatives:
- D3.js: More flexible but similarly complex
- Lightweight charting libraries: May not support all chart types
- Custom canvas rendering: Most work, most control

### Option 4: Accept Current Performance

**Rationale**: 19 seconds for 15 charts is reasonable
- Each chart takes ~1.3 seconds average
- CDN overhead with interception: 0ms (interception overhead negates benefit)
- Actual per-widget time: ~1.3 seconds

## Recommendation

**Do not deploy CDN localization to production.** 

Instead:
1. **Keep current CDN approach**: Cloudflare CDN is already efficient
2. **Document current performance**: 19 seconds for 15 charts is acceptable
3. **Monitor for regression**: Track performance over time
4. **Consider user experience**: Add progress indicator if not already present

## Technical Details

### Files Modified

1. `backend-service/Dockerfile` - Added Chart.js library
2. `backend-service/server.js` - Added CDN interception handler

### Rollback Steps

To revert to pre-optimization state:
```bash
# Remove Chart.js library
rm backend-service/lib/chart.umd.js

# Remove Dockerfile COPY line
# Remove server.js interception handler

# Rebuild and restart
docker build -t backend-service .
docker restart pdf-test
```

### Performance Metrics

| Metric | Before | After | Change |
|--------|--------|-------|--------|
| Total time | 22.9s | 27.3s | +4.4s (worse) |
| Widget render | 17.1s | 19.4s | +2.3s (worse) |
| Per-widget avg | ~1.1s | ~1.3s | +0.2s (worse) |

## Conclusion

The CDN localization optimization was based on the incorrect assumption that Chart.js download time was the bottleneck. In reality:

1. **Cloudflare CDN already serves Chart.js quickly** (<100ms)
2. **Chart.js execution is the real bottleneck** (2-2.5 seconds)
3. **Request interception adds overhead** that negates any benefit

The optimization should be reverted, and future optimization efforts should focus on:
- Chart.js execution optimization (difficult)
- Caching rendered charts (complex)
- Accepting current performance (practical)

---

**Analysis Date**: 2026-06-23
**Analyst**: AI Assistant (Claude Code)