LEARNING OBJECTIVES ⌵
- Understand why standard
fetch()lacks built-in upload progress telemetry and howXMLHttpRequest.uploadfills this gap. - Track byte transfer metrics (
event.loaded,event.total,event.lengthComputable) viaProgressEvent. - Render accessible progress indicators using the native
<progress>element and ARIAprogressbarattributes. - Implement user-initiated upload cancellation via
xhr.abort()andAbortSignal. - Build a drag-and-drop file staging area with client-side size, MIME type validation, and thumbnail previews.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine standing at a cargo dock loading 50 heavy steel shipping crates onto a ship. If you step into a dark tunnel, push all 50 crates into the darkness, and stand there waiting for 20 minutes in silence, you have zero visibility: Did a crate get stuck? Has 10% shipped or 90% shipped? If the ship leaves early, can you pull the emergency lever to stop?
Now imagine a digital scale on the conveyor belt equipped with a live LED dashboard. As each crate moves across the belt, the dashboard reads: "Crate 12 of 50 (24.5 MB / 100 MB — 24% complete — Speed: 2.1 MB/s — 36s remaining)", and beside you is a bright red Emergency Stop lever.
This is the power of the XMLHttpRequest.upload.onprogress API. While the standard fetch() API makes download progress simple, the classic XMLHttpRequest.upload interface provides the industry standard, cross-browser conveyor belt for real-time upload telemetry and instant abort capability.
Technical Deep Dive & Specifications
The Upload Telemetry Gap: Fetch vs XHR
Many modern developers ask: "Why use XMLHttpRequest when we have fetch()?"
+-------------------------------------------------------------------------------+
| DOWNLOAD PROGRESS vs UPLOAD PROGRESS |
+-------------------------------------------------------------------------------+
| Feature | Fetch API (Modern) | XMLHttpRequest (Classic)|
|----------------------|-------------------------------|-------------------------|
| Download Progress | ✅ Native (via ReadableStream) | ✅ Native (onprogress) |
| Upload Progress | ⚠️ Experimental (Streams only)| ✅ Rock-Solid (xhr.upload|
| Multipart Streaming | ⚠️ Complex piping | ✅ Automatic FormData |
| Cancel In-Flight | ✅ AbortController | ✅ xhr.abort() |
| Browser Support | Modern Only | 100% Universal |
+-------------------------------------------------------------------------------+
While WHATWG Fetch is specifying writable streams for request bodies, XMLHttpRequest.upload remains the production standard across FAANG and enterprise systems for tracking file upload progress.
The ProgressEvent Architecture
The xhr.upload target emits granular lifecycle events as bytes leave the client socket:
[ xhr.send(formData) ]
│
▼
[ xhr.upload.addEventListener('loadstart') ] ──> Initial connection opened
│
▼
[ xhr.upload.addEventListener('progress', e) ] ──> Fires repeatedly as chunks transmit
├── e.lengthComputable === true
├── e.loaded: Bytes transferred so far (e.g. 4,194,304)
└── e.total: Total bytes in payload (e.g. 16,777,216)
│
▼
[ Calculate Percentage: Math.round((e.loaded / e.total) * 100) ]
│
▼
[ xhr.upload.addEventListener('load') ] ──> All upload bytes transmitted to server
│
▼
[ xhr.addEventListener('load') ] ──> Server finished processing & returned HTTP response
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
// 1. Monitor upload stream
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
progressBar.value = percent;
statusText.textContent = `${percent}% uploaded (${(event.loaded / 1024 / 1024).toFixed(1)} MB)`;
}
});
// 2. Upload completed
xhr.upload.addEventListener('load', () => {
statusText.textContent = 'Upload complete. Processing on server...';
});
// 3. Server HTTP response returned
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Server response:', xhr.responseText);
}
});
The Native <progress> Element vs Accessible Custom Bars
HTML5 provides the semantic <progress> element. For custom styled UI, ensure standard WAI-ARIA attributes are present:
<!-- Native Semantic Progress -->
<progress id="upload-bar" value="45" max="100">45%</progress>
<!-- Custom Accessible Progress Component -->
<div
class="custom-progress"
role="progressbar"
aria-valuenow="45"
aria-valuemin="0"
aria-valuemax="100"
aria-label="File upload progress"
>
<div class="progress-fill" style="width: 45%;"></div>
</div>
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 82–86 (
<progress id="upload-bar" ...>): Native HTML5 progress element with standard min (0) and max (100) attributes. - Lines 111–114 (
dropZone dragover & drop):e.preventDefault()is mandatory in dragover to instruct the browser that the drop zone accepts files instead of navigating to the file URL. - Lines 126–135 (
stageFile(file)): Validates the selectedFileobject frome.dataTransfer.filesor<input type="file">, formats file size to MB, and reveals the telemetry panel. - Lines 159–175 (
Progress calculation & Speed estimation): Calculates the percentage(loaded / total) * 100and determines transfer velocity by measuring elapsed time viaperformance.now(). - Lines 178–186 (
btnCancel.onclick): InvokesactiveXhr.abort()or halts streaming, immediately stopping socket data flow.
Expected Browser Render Output
+-------------------------------------------------------------+
| Streaming File Uploader |
| |
| +---------------------------------------------------------+ |
| | 📁 Drag & Drop your file here | |
| | or click to browse from device | |
| +---------------------------------------------------------+ |
| |
| presentation-deck.mp4 |
| 14.80 MB (video/mp4) |
| [========================>................] |
| 64% (9.5 / 14.8 MB) 3.4 MB/s |
| |
| [ Cancel Upload ] |
+-------------------------------------------------------------+🏋️ Hands-On Exercise
🎯 The Challenge: Multi-File Sequential Batch Uploader
Instructions:
- Build a multi-file upload queue where users can select multiple files at once (
<input type="file" multiple>). - Display a list of queued files with individual status indicators (
Pending,Uploading...,Done,Failed). - Upload files sequentially (one file at a time) using
XMLHttpRequest. - As each file uploads, update its individual progress bar in real-time.
- Provide a global progress metric: "Uploaded File X of Y (Overall: Z%)".
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Computing Progress Without Checking
lengthComputable: If the HTTP payload size is indeterminate,event.lengthComputableisfalse, and computingloaded / totalyieldsInfinityorNaN. - Forgetting
e.preventDefault()ondragover: If you do not calle.preventDefault()inside adragoverhandler, the browser will open the dropped image/PDF in a new browser tab, destroying current form state. - Memory Leaks from Object URLs: When previewing files with
URL.createObjectURL(file), always revoke them viaURL.revokeObjectURL(url)when the upload completes or the image unmounts.
💡 Pro Tips
- Compute Real-Time Velocity & ETA: Measure byte delta over time delta (
(loaded - lastLoaded) / timeDelta) to calculate transfer rate in MB/s and estimate remaining seconds:Math.round((total - loaded) / speedBytesPerSec). - Listen to Both
upload.onloadandxhr.onload: Remember thatxhr.upload.onloadmeans bytes have finished leaving the client machine, whereasxhr.onloadmeans the server has received, processed, and responded to the payload.
📌 Key Takeaways
XMLHttpRequest.upload.onprogressis the cross-browser standard for tracking real-time client upload metrics.- Always check
event.lengthComputablebefore calculating percentage ratios. - Cancel active uploads instantly using
xhr.abort(). - Always call
event.preventDefault()ondragoveranddropevents to enable custom drop zones. - Clean up generated file previews using
URL.revokeObjectURL(). - --