I've played around with shaders in the past to build particle simulations with millions of points, and it's always really tickled my mind. You basically write functions that operate against a big 2d grid of colors. But because colors are represented by a 4x1 vector [r,g,b,a], you can repurpose this pattern to do general purpose computation (e.g. you can represent a point in a 3D coordinate space as a [r,g,b] color).
You can see this in the codepen in this post. It's literally creating "materials" and "render targets" that are actually just intermediate computation steps in the fluid simulation.
class Fluid {
constructor(context) {
this.context = context;
this.speed = 4;
this.forceInitMaterial = this.createShaderMaterial(forceInitFrag);
this.divergenceMaterial = this.createShaderMaterial(divergenceFrag);
...
this.divergence = this.createRenderTarget();
this.advection = this.createRenderTarget();
...