Postmortem: The 2500-Line File Was Fine Until We Split It
TLDR
A single 2,500-lineindex.html file containing inline JS worked smoothly in development. However, splitting it into discrete ES modules triggered three distinct failure modes: silent $(this) binding failures in arrow functions, initialization race conditions due to un-sequenced imports, and CSS rule order inversions. The modular split did not create these bugs. It exposed architectural defects that the single-file environment had hidden.
| Refactoring Incident | Monolith Environment Behavior | Post-Split Module Behavior | Root Cause |
|---|---|---|---|
| Trace Button Failure | Worked via coincidental global scope | $(this).data() returned undefined | Arrow function lexical scope |
| Theme Toggle Race | Initialized sequentially top-to-bottom | Read un-initialized state | Un-sequenced module loading |
| Accordion Styling | CSS rules loaded in single block | Visual styling broken | Split CSS load-order inversion |
Problem statement: the hidden safety net of single-file scripts
We maintained a 2,500-line single-file script containing canvas engine code, spatial indexing, UI bindings, and CSS styles.
Because everything executed sequentially top-to-bottom within a single file, the codebase appeared stable. We scheduled a refactoring sprint to split the monolith into proper ES modules.
Technical failure mode: simultaneous defect exposure
As soon as we split the script into 9 separate modules under core/, canvas/, and features/, three categories of silent defects surfaced:
- Arrow Function Scope Loss:
// DEFECT: Outer 'this' pointed to module instance, not the clicked button
$('#traceWireBtn').on('click', () => {
const mode = $(this).data('mode'); // Returned undefined!
self.setMode(mode);
});
- Initialization Race Conditions: The theme module read
localStorage.getItem('theme')to apply CSS classes, but the toggle UI module loaded first and read un-initialized DOM attributes. - CSS Load Order Inversions: Extracting inline styles into separate stylesheet files broke selector precedence dependencies.
The fix & architecture: explicit top-level initialization
We fixed element scope bindings by converting event handlers to standard function() syntax or using $(e.currentTarget).
To eliminate initialization races, we created a top-level orchestrator function in core/svgEditor.js that initializes modules in strict sequence:
// REFACTORED: Explicit module initialization sequence (core/svgEditor.js)
import { initTheme } from './theme.js';
import { initCanvas } from '../canvas/canvasEngine.js';
import { initLayers } from '../features/layers.js';
export function initApp() { // 1. Initialize data & theme state first initTheme();
// 2. Initialize canvas DOM elements initCanvas();
// 3. Initialize UI feature panels after DOM parity is established initLayers(); }
document.addEventListener('DOMContentLoaded', initApp);
Rule of thumb: Modular refactoring does not create scope bugs; it exposes latent defects hidden by single-file environments. Use explicit top-level orchestrators to control module initialization sequence.