requestAnimationFrame problem when using --optimize


#1

I’m doing some stuff with D3 and sending updates through ports. One port is having issues when I compile with --optimise—it works fine in debug mode however.

I need D3 to select the #map div and do work on it, but this is rendered in my View, so I use requestAnimationFrame to make sure the View is rendered before starting the D3 commands:

port drawMap : () -> Cmd msg
app.ports.drawMap.subscribe(function() {
     requestAnimationFrame(function() {
         drawMap();
     });
     return null;
});

This is before any post processing (i.e. minification), literally all I’m doing is adding --optimise to elm make. So this says to me that I’m doing something here that the compiler has stripped away as superfluous. Any ideas what I should be doing differently?


How would you make ports better?
#2

I might be missing something, but I’m not sure exactly what the problem is. You want to use rAF to time D3’s rendering and --optimize breaks it, but you didn’t explain exactly what is going wrong :sweat_smile:


#3

Does the behavior change if instead of requestAnimationFrame you use setTimeout(drawMap, 2000)? It would help in seeing if it is a timing bug, the debugged does slow down the application as the Model grows.


#4

By ‘breaks it’ I mean the ports’ function doesn’t seem to fire. drawMap should populate the #map div with data, instead it remains empty.

A little more background: in my model there is a flag which toggles separate views, this means that #map only exists some of the time. Ports.drawMap is called on init, and then through the update message that toggles the map’s view. When using --optimise I see the map on init, but toggling away from that view and then back again, the map does not display. No errors or anything, just an empty div.

So @pateh is correct, it’s a timing issue. setTimeout(drawMap, 2000) works, albeit slow. So should I just lower the timeout value to the minimum that functions? That doesn’t seem to be too solid of a solution. Perhaps there’s a way to check the div exists first?

Edit: This seems to work OK:

function waitForMap(callBack){
  window.setTimeout(function(){
    var element = document.getElementById('map');
    if(element){
      callBack();
    }else{
      waitForMap(callBack);
    }
  },50)
}

app.ports.drawMap.subscribe(function() {
    waitForMap(drawMap);
    return null;
});

Does that make sense or is there a better solution?


#5

Ah, I see. Very strange that --optimize changes that behavior. An SSCCE would be great, if you have time. A single rAF should be called right after Elm has updated the view, but I guess it’s being called before? Maybe you need two rAF in this case.

You could probably use requestAnimationFrame instead of setTimeout but the difference is not significant IMO.


#6

I had a similar issue in an app that I’m developing. (Specifically, I set up some ports to save the scroll position in localStorage when changing views, and then to scroll back to the previous position when returning to a previously-visited view. The page has to be completely rendered though before calling the return-to-position code). I solved it in a similar way that you did, except I used 0 as the timeout value. My understanding of what happens is this:

  1. Elm code requests the port be invoked (by returning the appropriate Cmd from the update fn)
  2. The Elm runtime runs the JS function associated to the port. At this time, the DOM has not been updated yet; it will be drawn on the next animation frame.
  3. Ergo, if we call requestAnimationFrame in the port function, there will be a race condition with the DOM update (because both are running on the same animation frame)
  4. Ergo2, we must call requestAnimationFrame, and then delay until after the animation frame callbacks are finished (but run as soon as possible after that)
  5. setTimeout with a delay of 0 accomplishes this
  6. (But the practical effect of this is that we take up two animation frames: one to update the DOM from Elm, and one to do things with the new DOM in JS. This was ok for my use case, since a slight delay to updating the scroll position is not really noticeable. It sounds like this is the case for you too – but this extra delay might become problematic for real time drawing.)

To be clear, I didn’t reach this understanding by reading the Elm kernel JS or tracing its execution, I deduced it from the behavior of code like yours (and mine). I’m laying it out explicitly here so it has a chance to get documented or hopefully even fixed (so that port callbacks execute after the DOM update, which I regard as more intuitive behavior).


#7

I totally agree with your assessment here. Point 3 seems to be the crux of the issue.
For the moment I’ve solved my specific case wrapping requestAnimationFrame inside a second requestAnimationFrame, so two frames have advanced before I try to write to the DOM.

As you say, this is not too time sensitive for me: I’m not too worried about this small delay.But I second the motion to perhaps investigate this behaviour in the core further—or at the very least add some official documentation about how to handle this correctly.


#8

I feel this is one of the most “complex” cases of the elm/js interop and it’s due to the async nature of the ports.
With the code you have in the first post you just cant’t know whether or not the div that drawMap wants will be there :smiley:
In a way you can always fix this by waiting or just recursing until you find it (the second snippet), but you can never be sure it will work (f.ex. try with a mobile device, or with the developer tools and with a slow cpu) in the first case and it’s not very resource effective in the second.

In our app we solved it with CustomElements and if you can I highly recommend that way, more info here: https://dev.to/lukewestby/talk-when-and-how-to-use-web-components-with-elm-f85
or ask if you need more details :smiley: