sabithpocker
sabithpocker

Reputation: 15566

How to flatten / debounce attribute changed callback in Web Components

I have a web component that is using attributeChangedCallback

attributeChangedCallback(name, oldValue, newValue) {
    // TODO: use something like rxjs debounce time?
    this.expensiveRenderer()
}

And I am setting new values for two attributes on each animation frame: Also, this can increase to setting 4 attributes.

component.setAttribute("att1", r);
component.setAttribute("att2", p);

This will trigger attributeChangedCallback twice and the expensive renderer is also triggered twice.

Is there an efficient way to set two attributes together, or make the effect of change as a single event similar to debounce time or so?

I am a bit skeptical on using setTimeout / clearTimeout as this is called on each animationFrame 60 fps.

To give a better overview, my component looks somewhat like:

<mm-spirograph
    fixed-circle-radius="100"
    moving-circle-radius="10"
    moving-circle-locus-length="30"
    repeat-count="100"
  ></mm-spirograph>

And it renders a spirograph with webGL and is planning to use for generative art. I like the simplicity of this and is a bit reluctant to use JSON attribute.

Also, animating the spirograph is kept separate from the component, the idea is to use spirograph as a static render or changing the attributes can easily do animation like. Here it is only animating two attributes but it can vary for different cases.

Also, there is a plan to add similar components like this which can be animated if needed by setting attributes.

function animateSpirograph(spirograph, r, p, rIncrement, pIncrement) {
  let v = r;
  if (v + rIncrement > 100) rIncrement = -Math.abs(rIncrement);
  if (v + rIncrement <= 0) rIncrement = Math.abs(rIncrement);
  v = v + rIncrement;
  r = v;
  let w = p;
  if (w + pIncrement > 200) pIncrement = -Math.abs(pIncrement);
  if (w + pIncrement <= 0) pIncrement = Math.abs(pIncrement);
  w = w + pIncrement;
  p = w;
  spirograph.setAttribute("moving-circle-radius", r);
  spirograph.setAttribute("moving-circle-locus-length", p);
  window.requestAnimationFrame(() =>
    animateSpirograph(spirograph, r, p, rIncrement, pIncrement)
  );
}

What Danny suggested is interesting, I can have a third attribute that takes in may be the timestamp from requestAnimationFrame and mark it as an optional attribute used only for animation. So every time attributes are changed we need to set this extra attribute to actually trigger the render. But this sounds a bit hacky/patch.

Upvotes: 2

Views: 751

Answers (1)

Intervalia
Intervalia

Reputation: 10945

Use a timeout. This allows your entire code to execute before doing the expensive rendering.

class MyEl extends HTMLElement {
  constructor() {
    super();
    this.renderTimeout = null;
    this.renderCalled = 0;
    this.renderDone = 0;
  }
  
  static get observedAttributes() {
    return ['time','direction'];
  }

  expensiveRenderer() {
    this.renderCalled++;
    if (this.renderTimeout) {
      clearTimeout(this.renderTimeout);
    }
    
    this.renderTimeout = setTimeout(
      () => {
        this.renderDone++;
        this.innerHTML = `<p>The time is: ${this._time}</p><p>And the direction is ${this._direction}</p><p>Render called: ${this.renderCalled}</p><p>Rendered: ${this.renderDone}</p>`;
      }, 1
    );
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this[`_${name}`] = newValue;
      this.expensiveRenderer();
    }
  }
}

customElements.define('my-el', MyEl);

const component = document.querySelector('my-el');

setTimeout(() => {
  component.setAttribute("time", "1:00");
  component.setAttribute("time", "2:00");
  component.setAttribute("time", "3:00");
  component.setAttribute("time", "4:00");
  component.setAttribute("direction", "East");
  component.setAttribute("direction", "West");
  component.setAttribute("direction", "South");
}, 2000);
<my-el time="12:00" direction="North"></my-el>

In this we set the attributes several times and we call the function many times. But you will see that we only actually do the fake-expensive rendering routine twice. (Maybe three times if the DOM parser takes extra time to parse your initial HTML.

setTimeout can happen as soon as the next event cycle. And must faster then 60 times a second. I use a timeout of 0 or 1 to place the event as the next thing in the queue. Yes, other things may happen before the callback, but it should still be in the sub-second time-frame.

requestAnimationFrame happens at the refresh time-frame, which is often 60 times a second.

Upvotes: 2

Related Questions