p5.js icon indicating copy to clipboard operation
p5.js copied to clipboard

Enable lazy loading in setup

Open quinton-ashley opened this issue 1 month ago • 14 comments

Increasing access

Making lazy loading in parallel possible in setup, in a way that's already familiar to users of p5 v1, would enable beginners to make sketches that use images and other assets without having to learn about async/await yet.

EDIT: Lazy loading is a tactic to reduce loading times, thereby reducing bounce rates, especially in the context of games and long form sketches, in which many assets are not needed immediately when the sketch loads. A goal of offering this feature is to get students in the mindset that they should generally expedite the initial load of their sketch by not halting it to load stuff first.

Most appropriate sub-area of p5.js?

  • [ ] Accessibility
  • [ ] Color
  • [x] Core/Environment/Rendering
  • [ ] Data
  • [ ] DOM
  • [ ] Events
  • [ ] Image
  • [ ] IO
  • [ ] Math
  • [ ] Typography
  • [ ] Utilities
  • [ ] WebGL
  • [ ] Build process
  • [ ] Unit testing
  • [ ] Internationalization
  • [ ] Friendly errors
  • [ ] Other (specify if possible)

Feature request details

I'm not trying to retread old ground here 😅 but yesterday I found out about an interesting feature of JavaScript that can enable both async/await and lazy loading in setup! 🤩

Currently in p5 v2 lazy loading can be done like this, which requires async/await.

let dog;

async function setup() {
  createCanvas(200, 200);

  // not awaited
  lazyLoading();
}

async function lazyLoading() {
  dog = await loadImage('dog.png');
}

function draw() {
  image(dog, 100, 100);
}

Currently in p5 v2 the following example wouldn't work because loadImage returns a promise, not the p5.Image object itself. dog would be a promise and it would not be displayed after it loads.

let dog;

function setup() {
  createCanvas(200, 200);
  dog = loadImage('dog.png');
}

function draw() {
  image(dog, 100, 100);
}

But it would work if loadImage returns a p5.Image object with a then method. That way, beginners could lazy load images in setup before learning about async/await! 🥳

After users learn async/await, they could very easily choose whether to load an asset sequentially (with await) or via lazy loading.

let cat, dog;

async function setup() {
  createCanvas(200, 200);

  cat = await loadImage('cat.png');

  dog = loadImage('dog.png');
}

function draw() {
  image(cat, -100, 100);
  image(dog, 100, 100);
}

This feature also could provide a better way for p5 v2 to do preload compatibility while also supporting async/await at any point in the sketch, which I previously thought was impossible. loadImage would not need to be changed from promise returning to object returning, it could simply return an object with a then method. Magic!

@ksen0 @davepagurek @kjhollen Does this change things? Do you still want to require beginners to use async/await? EDIT: More importantly, is this a good way to support lazy loading in p5 v2?

I'm going to implement this feature in q5.js and share working examples soon!

quinton-ashley avatar Nov 16 '25 14:11 quinton-ashley

One of the problems with returning an object immediately is that some methods, like loadJSON, don't know what their return value should be immediately. p5 1.x always returns an object, making it so you can't really load an array that way, because once you've returned an object, it's very hard to turn it into an array.

Would this be a special case just for images?

davepagurek avatar Nov 16 '25 14:11 davepagurek

@davepagurek Ah yeah true, so perhaps just for p5 load method that return a single type of object, like loadImage.

Also I learned that then methods on instances are not able to return the instance itself. Dang!

So what'd be needed behind the scenes is to create a copy of the loaded object and resolve with that. I'll try to make a minimal working example of how this could be done with image data.

quinton-ashley avatar Nov 16 '25 16:11 quinton-ashley

Here's a standalone proof of concept!

https://codevre.com/editor?project=bTC7gjpSpkTgfyD7VRiZdh3iJFw2_20251116172303722_knhf

Pretty complicated lol but it does work well. I'll try implementing this strategy in q5 now.

quinton-ashley avatar Nov 16 '25 17:11 quinton-ashley

hmm, if this pattern doesn't apply across all loadX functions then I'm not sure I see the utility here—exceptions to patterns are tough for early learners. If we can make this work for all loadX functions, then it sounds like a good addition, but otherwise I think it invites confusion.

Some early API/library decisions were about what was available and well documented at the time, rather than whether or not they were considered beginner friendly, and I think this is one of them. Initial development for p5.js started in 2013 and became a more widely usable alpha/beta in 2015, so even ES6 features weren't widely in use then.

async/await is used so widely now in JavaScript that it feels valuable to introduce in a minimal/friendly way to give students some idea of what this means when they encounter it in other documentation, google searches, etc. I think beginners can and do accept really simple explanations like "setup gets async in front of it because we might need to load files or data there" and "use await in front of loadX functions because they need some time to read files or grab data from a web address." This is a fairly easy pattern to memorize for a beginner, even without a deeper explanation of promises.

kjhollen avatar Nov 16 '25 19:11 kjhollen

@kjhollen For IO loadX functions (like loadJSON, loadStrings, etc.) they could all have a clearly defined pattern of returning promises, since lazy loading that stuff isn't practically desirable anyway.

But loadPixels already breaks the pattern, it does not return a Promise.

Having loadFont, loadImage, and loadSound return then-able objects would enable lazy loading in parallel, the same way it was done in p5 v1, while also making these functions await-able.

Again, my intention is not to keep having debates regarding teacher preferences on when to introduce async/await. Let's agree to disagree and collectively accept that depending on how some teachers use p5.js they might have different preferences. Is that okay?

I'd like to keep the focus of this feature request discussion on whether this proposal is a good way to implement lazy loading in parallel vs any current alternative ways in p5 v2.

Sorry, perhaps I should've explained why lazy loading is actually desirable in common use cases (not just for beginners to avoid using async/await).

In the context of intermediate users making longer sketches, especially games, some assets really don't need to be waited for cause they'll never be used at the start of the program.

By teaching students lazy loading first and then introducing the idea "use await if you really want something to fully load at the start of your sketch", it gets students in the mindset that halting the progress of their sketch to load stuff is generally good to avoid, to keep loading times short.

This proposal provides users a very simple and quick way to load assets in parallel without preload, Promise.all, or bad workarounds.

To illustrate the downside of making await required just to get a variable reference, see how complex it is to do lazy loading of just two images in p5 v2.

let dog, cat;

async function setup() {
  createCanvas(200, 200);

  // not awaited
  lazyLoading1();
  lazyLoading2();
}

async function lazyLoading1() {
  dog = await loadImage('dog.png');
}

async function lazyLoading2() {
  cat = await loadImage('cat.png');
}

Or with Promise.all:

let dog, cat;

async function setup() {
  createCanvas(200, 200);

  // not awaited
  lazyLoading();
}

async function lazyLoading() {
  [ dog, cat ] = await Promise.all([
     loadImage('dog.png'),
     loadImage('cat.png')
   ]);
}

Vs how it was done in p5 v1 and how it could be done in p5 v2.

let dog, cat;

async function setup() {
  createCanvas(200, 200);

  dog = loadImage('dog.png');
  cat = loadImage('cat.png');
}

Is there a better alternative? I can't think of any.

quinton-ashley avatar Nov 17 '25 16:11 quinton-ashley

By teaching students lazy loading first and then introducing the idea "use await if you really want something to fully load at the start of your sketch", it gets students in the mindset that halting the progress of their sketch to load stuff is generally good to avoid, to keep loading times short.

I appreciate you making this clearer! I responded to the initial proposal for increasing access here, which said that this would make it easier for all beginners, but it sounds like the use case is actually pretty specific to teaching asset loading in game design. Would you please update the issue description to clarify?

Are you usually teaching with p5play when you encounter this use case? Is there a way you can implement something that feels like lazy loading in p5play (e.g. with a utility function in p5play that takes a list of images as arguments and loads them using Promise.all behind the scenes) without this feature in p5?

kjhollen avatar Nov 17 '25 17:11 kjhollen

I wanted to share a thought from an accessibility perspective. I think about universal design principles, which emphasize support for multiple approaches to benefit accessibility. When I see debates on what is/isn't beginner friendly, I think the goal of arriving on one conclusion ignores the diversity of who uses these tools.

I don't know if this particular proposal is a fit for this library, but what I like about it is that it offers multiple approaches. It would be impossible for p5 alone to support the entire wide range of approaches that would best serve this community, so there's a balance to strike.

Beyond just this issue, I would love to see new thinking on how to make p5's very particular design more easily compatible with a learner/beginner who would benefit from doing some things differently. The support for add-ons like p5.play is a great foundation for this, but the process of finding and installing add-ons itself can be tricky. I'm wondering if there are new ways to streamline the process of a creator mixing and matching the tools that best support their thinking and process.

When a student struggles with asynchronous programming, it would be great for a teacher to have a minimal hassle way to be able to offer another option. Whether that's something that p5 supports via a feature like this or is supported by the larger community, I think that type of application of universal design would greatly benefit the whole group.

calebfoss avatar Nov 17 '25 17:11 calebfoss

@kjhollen Yes, sorry! I should've included that info. I will update the feature description.

p5play v3 was designed heavily around the preload system and can't work without it, so I'm just anticipating this being an issue with q5play (p5play v4) which I want to work with p5 v2.

I do appreciate that p5 v2 has async/await loading but currently the way it's been done is quite limiting. In p5 v1, yes it was a pain to wait for specific assets to load and do stuff afterwards via callback hell, but it was easier to do parallel preloading in preload and parallel lazy loading in setup. I don't want that flexibility to be lost.

You'd be surprised how much assets students load in their games! So I'm concerned about the effect of page loading times on bounce rates (the amount of people that leave a website because they take too long to load).

Image

Source: ThinkWithGoogle

Page loading needs to be something q5play users think of during development, not an afterthought when they release the game and players complain that it took forever to load (or never even play the game cause they didn't want to wait).

This could be something I just implement in q5play, but I think some changes would need to be made to p5 v2 to accommodate an addon being capable of adding it. I'd prefer this feature to be part of p5 v2 directly.

quinton-ashley avatar Nov 17 '25 17:11 quinton-ashley

Thanks for the additional insights, @calebfoss @quinton-ashley. I don't mean to imply there is a one-size-fits all solution to talking about async/await. As I said above, I think if we can implement the feature being requested here for all loadX IO functions using features that were not available in JavaScript in the early days of p5, that seems like a great addition.

When I also saw that the implementation was described as complicated and that someone noted it would not be possible with some loadX IO functions, I thought it was worth discussing alternatives and trying to understand the use case better. Introducing more inconsistency is what I'm worried about, because I think inconsistency is tough for teachers and learners at all levels.

With more time we may be able to work out a better implementation that is consistent or other changes to the API that communicate the inconsistencies well. If there is an immediate need for faster load times in p5play and another solution is available in the interim, I think there's no need to rush here so we can think through all of the implications of this proposal.

Thanks for inviting me to the discussion, @quinton-ashley. I don't have a lot of capacity for contributing to p5.js these days, so I probably won't be able to comment further for a while.

kjhollen avatar Nov 17 '25 19:11 kjhollen

Is it a requirement that we overload the existing load* functions for this? e.g. if we're ok having both loadImage and lazyLoadImage, then one could tell users that the lazyLoad* version is never awaited if it exists (and then we just don't have a lazy version of data loading.)

Implementation could be basically what the preload compatibility addon does here, but instead of overriding the same function, it would create a new one.

davepagurek avatar Nov 17 '25 20:11 davepagurek

@davepagurek Ah! Interesting, that's like going about it from the other direction.

For others to reference, the p5 v2 preload compat addon basically does this:

function loadImage(...args) {
  const obj = new p5.Image(1, 1);
  
  const promise = _loadImageWithPromise.apply(this, args).then((result) => {
    for (const key in result) {
      obj[key] = result[key];
    }
  });
  
  promises.push(promise);
  return obj;
}

It creates an empty image object which gets passed to the user immediately, then has the og promise based loadImage make a new image, and once that loads its contents get copied to the original object.

Okay so I was wrong. It'd actually be easy, within q5play, to override some loadX functions to return a then-able (await-able) object, without any changes to p5 v2. Awesome!

So now I guess y'all could decide whether this feature should be exclusive to q5play or is something that the broader p5.js community could benefit from as well.

Also I'd def prefer doing this in a way that's familiar to p5 v1 users, making the loadX functions more flexible, without adding specialized functions for lazing loading to the API.

quinton-ashley avatar Nov 17 '25 22:11 quinton-ashley

@calebfoss I do think on the flip side, sometimes offering too many different ways to do the same thing can be overwhelming and confusing, which @ksen0 has pointed out in previous discussions.

That's why I disliked learning Java cause it's riddled with legacy crusty old ways to do stuff, new ways to do stuff, alternative ways that require an extra library, etc.

I do think p5.js partly excels cause pretty often it's like, "here's the one way to do this", and there's beauty in that kind of simplicity.

The trouble with that approach arises when the one-size-fits-all approach may not work well enough in some cases. Students loading 30-50 separate assets for their game is not uncommon.

quinton-ashley avatar Nov 17 '25 22:11 quinton-ashley

@kjhollen - I'm sorry, I didn't mean to point the finger at you. My intention was to speak to the larger discussion on features like this. I'm also thinking about my own tendency to want to solve problems with one perfect solution, which is not always productive.

And yes, @quinton-ashley, there is also a balance to strike with multiple approaches not becoming overwhelming. What I'm trying to say is that sometimes the case that doesn't work is not a matter of the technical side of the situation but rather the human side. Something I love about this community is the interest in making tools work for lots of different humans over requiring humans to use tools one certain way.

Pardon my drifting from the main topic here, but just speaking generally, I am encouraging the pursuit of opportunities to flexibly accommodate different approaches for the accessibility benefits.

calebfoss avatar Nov 17 '25 23:11 calebfoss

You guys still interested in implementing this to be part of p5 features ? if so i can start working on it, following what @davepagurek suggested is instead of overriding the existing method i will create the lazyLoad* version

AhmedMagedC avatar Nov 21 '25 20:11 AhmedMagedC