Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/upload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ Once installed, import the component in your application:
import '@vaadin/upload';
```

## Performance Considerations

When uploading large numbers of files, the component automatically throttles concurrent uploads to prevent browser performance degradation. By default, a maximum of 3 files are uploaded simultaneously, with additional files queued automatically.

You can customize this limit using the `max-concurrent-uploads` attribute:

```html
<!-- Limit to 5 concurrent uploads -->
<vaadin-upload max-concurrent-uploads="5"></vaadin-upload>
```

```js
// Or set it programmatically
upload.maxConcurrentUploads = 5;
```

This helps prevent:
- Browser XHR limitations (failures when uploading 2000+ files simultaneously)
- Performance degradation with hundreds of concurrent uploads
- Network congestion on slower connections

The default value of 3 balances upload performance with network resource conservation.

## Contributing

Read the [contributing guide](https://vaadin.com/docs/latest/contributing) to learn about our development process, how to propose bugfixes and improvements, and how to test your changes to Vaadin components.
Expand Down
10 changes: 10 additions & 0 deletions packages/upload/src/vaadin-upload-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ export declare class UploadMixinClass {
*/
uploadFormat: UploadFormat;

/**
* Specifies the maximum number of files that can be uploaded simultaneously.
* This helps prevent browser performance degradation and XHR limitations when
* uploading large numbers of files. Files exceeding this limit will be queued
* and uploaded as active uploads complete.
* @attr {number} max-concurrent-uploads
* @default 3
*/
maxConcurrentUploads: number;

/**
* The object used to localize this component. To change the default
* localization, replace this with an object that provides all properties, or
Expand Down
87 changes: 87 additions & 0 deletions packages/upload/src/vaadin-upload-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,20 @@ export const UploadMixin = (superClass) =>
value: 'raw',
},

/**
* Specifies the maximum number of files that can be uploaded simultaneously.
* This helps prevent browser performance degradation and XHR limitations when
* uploading large numbers of files. Files exceeding this limit will be queued
* and uploaded as active uploads complete.
* @attr {number} max-concurrent-uploads
* @type {number}
*/
maxConcurrentUploads: {
type: Number,
value: 3,
sync: true,
},

/**
* Pass-through to input's capture attribute. Allows user to trigger device inputs
* such as camera or microphone immediately.
Expand All @@ -347,6 +361,18 @@ export const UploadMixin = (superClass) =>
_files: {
type: Array,
},

/** @private */
_uploadQueue: {
type: Array,
value: () => [],
},

/** @private */
_activeUploads: {
type: Number,
value: 0,
},
};
}

Expand Down Expand Up @@ -698,12 +724,48 @@ export const UploadMixin = (superClass) =>
Array.prototype.forEach.call(files, this._uploadFile.bind(this));
}

/**
* Process the upload queue by starting uploads for queued files
* if there is available capacity.
* @private
*/
_processQueue() {
// Process as many queued files as we have capacity for
while (this._uploadQueue.length > 0 && this._activeUploads < this.maxConcurrentUploads) {
const nextFile = this._uploadQueue.shift();
if (nextFile && !nextFile.complete && !nextFile.uploading) {
this._uploadFile(nextFile);
}
}
}

/** @private */
_uploadFile(file) {
if (file.uploading) {
return;
}

// Check if we've reached the concurrent upload limit
if (this._activeUploads >= this.maxConcurrentUploads) {
// Add to queue if not already queued
if (!this._uploadQueue.includes(file)) {
this._uploadQueue.push(file);
file.held = true;
file.status = this.__effectiveI18n.uploading.status.held;
this._renderFileList();
}
return;
}

// Remove from queue if it was queued
const queueIndex = this._uploadQueue.indexOf(file);
if (queueIndex >= 0) {
this._uploadQueue.splice(queueIndex, 1);
}

// Increment active uploads counter
this._activeUploads += 1;

const ini = Date.now();
const xhr = (file.xhr = this._createXhr());

Expand Down Expand Up @@ -745,7 +807,13 @@ export const UploadMixin = (superClass) =>
if (xhr.readyState === 4) {
clearTimeout(stalledId);
file.indeterminate = file.uploading = false;

// Decrement active uploads counter
this._activeUploads -= 1;

if (file.abort) {
// Process queue even on abort
this._processQueue();
return;
}
file.status = '';
Expand All @@ -759,6 +827,8 @@ export const UploadMixin = (superClass) =>
);

if (!evt) {
// Process queue even if event was cancelled
this._processQueue();
return;
}
if (xhr.status === 0) {
Expand All @@ -776,6 +846,9 @@ export const UploadMixin = (superClass) =>
}),
);
this._renderFileList();

// Process the queue to start the next upload
this._processQueue();
}
};

Expand Down Expand Up @@ -877,10 +950,24 @@ export const UploadMixin = (superClass) =>
);
if (evt) {
file.abort = true;

// Remove from queue if it was queued
const queueIndex = this._uploadQueue.indexOf(file);
if (queueIndex >= 0) {
this._uploadQueue.splice(queueIndex, 1);
}

// Decrement active uploads if file was uploading
if (file.uploading) {
this._activeUploads -= 1;
}

if (file.xhr) {
file.xhr.abort();
}
this._removeFile(file);
// Process the queue to start the next upload
this._processQueue();
}
}

Expand Down
11 changes: 8 additions & 3 deletions packages/upload/test/adding-files.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('adding files', () => {

beforeEach(async () => {
upload = fixtureSync(`<vaadin-upload></vaadin-upload>`);
upload.target = 'http://foo.com/bar';
upload.target = 'https://foo.com/bar';
upload._createXhr = xhrCreator({ size: testFileSize, uploadTime: 200, stepTime: 50 });
await nextRender();
files = createFiles(2, testFileSize, 'application/x-octet-stream');
Expand Down Expand Up @@ -332,12 +332,17 @@ describe('adding files', () => {

describe('start upload', () => {
it('should automatically start upload', () => {
upload.maxConcurrentUploads = 1;
const uploadStartSpy = sinon.spy();
upload.addEventListener('upload-start', uploadStartSpy);

files.forEach(upload._addFile.bind(upload));
expect(uploadStartSpy.calledTwice).to.be.true;
expect(upload.files[0].held).to.be.false;
// With queue behavior, only the first file starts uploading immediately
expect(uploadStartSpy.calledOnce).to.be.true;
// Files are prepended, so the first file added is at index 1
expect(upload.files[1].held).to.be.false;
// Second file (at index 0) should be held in queue
expect(upload.files[0].held).to.be.true;
});

it('should not automatically start upload when noAuto flag is set', () => {
Expand Down
Loading