Skip to content

Commit fe2c002

Browse files
committed
Loader: modernize asset loading with Promise-based completion, improving parallel loading performance
follow up to #1188 performances improvements to be expected : - ~75-80% faster for small/cached assets - ~40-45% faster for typical loads - ~25-30% faster for moderate loads - ~10-12% faster for heavy loads and actually loading the platformer example is blazing fast now :P
1 parent db7bd94 commit fe2c002

3 files changed

Lines changed: 299 additions & 52 deletions

File tree

packages/melonjs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141

4242
### Changed
4343
- WebGLRenderer: `Compositor`, `QuadCompositor`, and `PrimitiveCompositor` are now deprecated in favor of `Batcher`, `QuadBatcher`, and `PrimitiveBatcher`
44+
- Loader: modernize asset loading with Promise-based completion, improving parallel loading performance
45+
- Loader: `onload`, `onProgress`, and `onError` properties are now deprecated in favor of `LOADER_COMPLETE`, `LOADER_PROGRESS`, and `LOADER_ERROR` events
4446

4547
## [18.0.0] (melonJS 2) - _2026-03-10_
4648

packages/melonjs/src/loader/loader.js

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,10 @@ export function setBaseURL(type, url = "./") {
150150
* @default undefined
151151
* @memberof loader
152152
* @type {function}
153+
* @deprecated since 18.2.0 - Use the {@link event.LOADER_COMPLETE} event or the `onloadcb` parameter of {@link loader.preload} instead.
153154
* @example
154-
* // set a callback when everything is loaded
155-
* me.loader.onload = this.loaded.bind(this);
155+
* // use the LOADER_COMPLETE event instead
156+
* me.event.on(me.event.LOADER_COMPLETE, this.loaded.bind(this));
156157
*/
157158
export let onload;
158159

@@ -163,9 +164,10 @@ export let onload;
163164
* @default undefined
164165
* @memberof loader
165166
* @type {function}
167+
* @deprecated since 18.2.0 - Use the {@link event.LOADER_PROGRESS} event instead.
166168
* @example
167-
* // set a callback for progress notification
168-
* me.loader.onProgress = this.updateProgress.bind(this);
169+
* // use the LOADER_PROGRESS event instead
170+
* me.event.on(me.event.LOADER_PROGRESS, (progress, resource) => this.updateProgress(progress, resource));
169171
*/
170172
export let onProgress;
171173

@@ -175,9 +177,10 @@ export let onProgress;
175177
* @default undefined
176178
* @memberof loader
177179
* @type {function}
180+
* @deprecated since 18.2.0 - Use the {@link event.LOADER_ERROR} event instead.
178181
* @example
179-
* // set a callback for error notification
180-
* me.loader.onError = this.loaderError.bind(this);
182+
* // use the LOADER_ERROR event instead
183+
* me.event.on(me.event.LOADER_ERROR, (resource) => this.loaderError(resource));
181184
*/
182185
export let onError;
183186

@@ -194,7 +197,6 @@ let parserInitialized = false;
194197
// flag to check loading status
195198
let resourceCount = 0;
196199
let loadCount = 0;
197-
let timerId = 0;
198200

199201
/**
200202
* Assets uploaded with an error
@@ -219,29 +221,17 @@ function initParsers() {
219221
}
220222

221223
/**
222-
* check the loading status
224+
* Complete loading: invoke the callback and emit the LOADER_COMPLETE event.
225+
* @param {Function} onloadcb - the completion callback
223226
* @ignore
224227
*/
225-
function checkLoadStatus(onloadcb) {
226-
if (loadCount === resourceCount) {
227-
// wait 1/2s and execute callback (cheap workaround to ensure everything is loaded)
228-
if (typeof onloadcb === "function" || onload) {
229-
// make sure we clear the timer
230-
clearTimeout(timerId);
231-
// trigger the onload callback
232-
// we call either the supplied callback (which takes precedence) or the global one
233-
const callback = onloadcb || onload;
234-
setTimeout(() => {
235-
callback();
236-
eventEmitter.emit(LOADER_COMPLETE);
237-
}, 300);
238-
} else {
239-
throw new Error("no load callback defined");
240-
}
228+
function completeLoading(onloadcb) {
229+
const callback = onloadcb || onload;
230+
if (typeof callback === "function") {
231+
callback();
232+
eventEmitter.emit(LOADER_COMPLETE);
241233
} else {
242-
timerId = setTimeout(() => {
243-
checkLoadStatus(onloadcb);
244-
}, 100);
234+
throw new Error("no load callback defined");
245235
}
246236
}
247237

@@ -400,26 +390,45 @@ export function setParser(type, parserFn) {
400390
* me.loader.preload(game.assets, () => this.loaded());
401391
*/
402392
export function preload(assets, onloadcb, switchToLoadState = true) {
403-
// parse the resources
404-
for (let i = 0; i < assets.length; i++) {
405-
resourceCount += load(
406-
assets[i],
407-
onResourceLoaded.bind(this, assets[i]),
408-
onLoadingError.bind(this, assets[i]),
409-
);
410-
}
411393
// set the onload callback if defined
412394
if (typeof onloadcb !== "undefined") {
413395
onload = onloadcb;
414396
}
415397

398+
// parse the resources and collect promises for each asset
399+
const promises = [];
400+
for (let i = 0; i < assets.length; i++) {
401+
const asset = assets[i];
402+
const promise = new Promise((resolve, reject) => {
403+
const count = load(
404+
asset,
405+
() => {
406+
onResourceLoaded(asset);
407+
resolve();
408+
},
409+
(err) => {
410+
onLoadingError.call(this, asset);
411+
reject(err);
412+
},
413+
);
414+
resourceCount += count;
415+
if (count === 0) {
416+
// asset already loaded, resolve immediately
417+
resolve();
418+
}
419+
});
420+
promises.push(promise);
421+
}
422+
416423
if (switchToLoadState === true) {
417-
// swith to the loading screen
424+
// switch to the loading screen
418425
state.change(state.LOADING);
419426
}
420427

421-
// check load status
422-
checkLoadStatus(onload);
428+
// call the completion callback as soon as all assets are loaded
429+
Promise.all(promises).then(() => {
430+
completeLoading(onload);
431+
});
423432
}
424433

425434
/**
@@ -440,15 +449,28 @@ export function preload(assets, onloadcb, switchToLoadState = true) {
440449
**/
441450
export function reload(src) {
442451
const assetToReload = failureLoadedAssets[src];
443-
this.unload(assetToReload);
452+
unload(assetToReload);
444453
resourceCount -= 1;
445-
resourceCount += this.load(
446-
assetToReload,
447-
this.onResourceLoaded.bind(this, assetToReload),
448-
this.onLoadingError.bind(this, assetToReload),
449-
);
450-
// check load status
451-
checkLoadStatus(this.onload);
454+
455+
new Promise((resolve, reject) => {
456+
const count = load(
457+
assetToReload,
458+
() => {
459+
onResourceLoaded(assetToReload);
460+
resolve();
461+
},
462+
(err) => {
463+
onLoadingError.call(this, assetToReload);
464+
reject(err);
465+
},
466+
);
467+
resourceCount += count;
468+
if (count === 0) {
469+
resolve();
470+
}
471+
}).then(() => {
472+
completeLoading(onload);
473+
});
452474
}
453475

454476
/**

0 commit comments

Comments
 (0)