Blog-Archiv

Sonntag, 29. Mai 2016

JS Browser Reflow and Repaint

You can do a lot of things in a web-page using JavaScript. For example, increase its load- or update-time significantly.
I am talking about JS statements that cause the browser to do long-lasting work. The classic reflow-pitfall is looping a lot of DOM-elements, reading the element's clientWidth, and then setting its style.width. The browser would have to renew its layout, what he does at least when the next clientWidth is read.

Which CSS-property change triggers which browser action is documented on csstriggers.com and a lot of other good good articles. Generally a reflow or repaint is forced by

  • insert, update, delete or move of a DOM element
  • animation of a DOM element
  • reading certain element-properties like offsetHeight, clientHeight etc.
  • calling getComputedStye() on an element
  • changing the style or class attribute of an element
  • ....

In this Blog I will present a very simple JS module to get around this problem, written in pure JS (no jQuery).

Problem

The problem is the deferring of CSS-properties writes. That means, you read some reflow-critical DOM-property like offsetWidth, and then you have to defer the resulting CSS-write on element.style.width to a later time by calling a function, passing all necessary information as parameters. When that later time has come, the settings must be triggered explicitly. I call it repaint() here. That means, a concrete application using this module will have to call repaint() at some point in time.

Solution

Here is the "deferred repainter" module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    var deferredRepainter = function()
    {
      "use strict";
      
      var repaintList = [];
      
      var that = {};
      
      /**
       * Aggregates CSS properties for a later repaint() call.
       * @param element the element to set given CSS properties to on repaint.
       * @param cssMap the CSS properties to set to given element on repaint.
       */
      that.addForRepaint = function(element, cssMap) {
        repaintList.push({
          element: element,
          cssMap: cssMap
        });
      };
      
      /** Sets all aggregated CSS properties now. */
      that.repaint = function() {
        for (var i = 0; i < repaintList.length; i++) {
          var element = repaintList[i].element;
          var cssMap = repaintList[i].cssMap;
          for (var css in cssMap)
            if (cssMap.hasOwnProperty(css))
              element.style[css] = cssMap[css];
        }
        repaintList = [];
      };
      
      return that;
    };

This module factory creates a JS object that can aggregate updates for an element, and perform them in the order they were added as soon as repaint() gets called.

The addForRepaint() function lets add CSS settings for later usage. That way you can read properties from the DOM or CSS, and defer changes to later. You need to pass the target DOM-element as parameter, and a map of CSS properties with values.

Calling repaint() simply will go through the repaintList and apply all aggregated changes. Finally it clears the list, thus redundant calls to repaint() will do nothing.

Example Application

Instead of reading and writing one by one, the application would first do all reads and aggregate resulting writes, and then call repaint() to flush all the writes.

      var deferredRepaint = deferredRepainter();

      var clientWidthToCssWidth = function(element, size) {
        var style = window.getComputedStyle(element);
        if (style["box-sizing"] === "border-box")
          return size + window.parseInt(style["border-left-width"]) + window.parseInt(style["border-right-width"]);
        return size - window.parseInt(style["padding-left"]) - window.parseInt(style["padding-right"]);
      };

      var setWidth = function(element, clientWidth) {
        var width = clientWidthToCssWidth(element, clientWidth);
        var cssWidth = width+"px";

        deferredRepaint.addForRepaint(element, {
          "width": cssWidth,
          "max-width": cssWidth,
          "min-width": cssWidth
        });
      };


      var elements = ....;
      for (var i = 0; i < elements.length; i++) {
        var element = elements[i];
        setWidth(element, element.clientWidth + 4);
      }

      deferredRepaint.repaint();

This application would increase the width of some elements by 4 pixels. It uses a sophisticated way to turn a clientWidth into a CSS width: it calculates padding or border depending on the box-sizing property. Unfortunately calling getComputedStyle() causes the browser to reflow when some CSS was changed before.

To avoid triggering continuous reflows, the application uses an instance of the deferredRepainter module to defer the CSS writes via setWidth(). As soon as the loop has ended, deferredRepainter.repaint() is called, which actually sets the values.

This works under the assumption that a browser would not do a reflow when just a CSS property was written but nothing was read after. Should be true for most browsers.
When a browser does not comply to that, we would have to write a dynamical stylesheet with a different CSS-class for each of the elements, add this class to the element's classList, and then, on repaint, add that stylesheet to head. This would do a bulk update with just one statement.




Samstag, 21. Mai 2016

JS Semicolons

Recently I noticed the hard-to-read source code of bootstrap.js, and I investigated a little on the internet. What I found were two prominent JS authors, one representative of the old programmer generation, one of the young, and their conflict:

"That is insanely stupid code. .... Learn to use semicolons properly."
"I have learned to use them, that's why there isn't one present."

It was about the fact that a minifier did not correctly pack bootstrap.js source, because that source makes excessive usage of the freedom that the JS language provides. When you look into it, you see that actually there are no semicolons at all, except the ones in for-loops, and on "use strict". And there are also a lot of syntactical constructs you see nowhere else. You could say, they heavily try to be unique, or you could say, they have found a better way to use JS. For sure it is hard to read for the average JS programmer that needs to understand why his bootstrap dialog does not show up. But actually no one can say what is right or wrong here.

Until now I couldn't find out why bootstrap authors do not use semicolons. Here is how I interpret that attitude. Look at following JS source:

var a = function() {
  return
  5
};
var b = function() {
  return
  5;
};
var c = function() {
  return 5
};

alert("a(): "+a()+", b(): "+b()+", c(): "+c());

Output is:

a(): undefined, b(): undefined, c(): 5

In this code, you have a() and b() doing the wrong thing, due to JS auto-semicolon-insertion, but c() doing the right thing, although the semicolon is missing.

The question rises:

when the semicolon did not fix the problem in b(), why use it at all?

What we are expected to learn here: semicolon is only a statement separator, not a statement terminator.

Some authors recommend to put semicolons just before an expression that starts with one of +-/[(.
Some recommend to put them everywhere.
Some recommend to leave them out completely.

I am not a JS expert, just a user. My primary goal is to write readable, comprehensible and maintainable source code. But I admit that I also have developed my personal style to write JS code. And I assume that every JS programmer has.

The big freedom of expression in the JavaScript language leads to a lot of different dialects. That means, you might not be able to read or understand the JS code of your fellow developer. I call this a communication bottleneck.

If you have read my older Blogs about all these problems in JS, you may understand my conclusion that ...

JS is not a language for corporate development.

The punchline here is that bootstrap originally was created to better organize corporate work within Twitter :-)

We are not alone on this planet, so let us not forget that communication is the thing that makes us successful. When we want better new solutions, we need to learn from the mistakes of the old ones.


P.S.
You want to know why they start each IIFE function with a plus-sign?
By doing so they avoid to write the enclosing parentheses. It's a trick to put the JS interpreter into the same parsing state.

P.P.S.
Yes, great idea, we also should drop indentations. Spaces are doing nothing, so why write them. Maybe we even can get rid of that programming language?




Sonntag, 15. Mai 2016

CSS Width 100% and Position Absolute or Fixed

The CSS statement "width: 100%;" does not always do what it's expected. In case of CSS position: absolute or fixed it does not.

The Symptom

Elements that have position: fixed, or position: absolute, and have padding, margin, or border, are too wide. See screenshot below.

Here is the HTML source code:

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Fixed Position with 100% Width</title>
    
  </head>
  
  <body>
    
    <div style="position: fixed; left: 0; top: 0; width: 100%; border: 1em solid red;">
      style = "position: fixed; width: 100%;"
    </div>

  </body>

</html>


Test Panel

This is a test panel that lets style the red-blue DIV element on bottom. That element has been given z-index: -1 to avoid it covering any input-field. As parent for position: absolute I have defined this chapter, enclosed by the dotted border. The parent for position: fixed is always the browser window. A DIV element is a block-element and has by default 100% width when positioned static or relative.

Set position to absolute or fixed, and then try to achieve a precise 100% width of the element. When you think you are done, add a margin and see whether it holds. Should you find a solution here, you don't need to work through this Blog :-)


position
width 100% (explicit)
box-sizing
top em
left em
bottom em
right em
margin em
border-width em
padding em


This DIV is a block element, having by default 100% width.


Learning by Try and Error


Position absolute and fixed dismiss default width

Set position to absolute. The element loses its default width of 100%, because it goes out of the document layout flow. Mind that when one of top or bottom is set to an explicit value, the element would go there, relatively to its next non-static parent (the Test Panel chapter).

The same happens when you set position to fixed. Mind that, in this case, the element disappears when none of top or bottom and left or right is set. So enter 0 for top and left, and the element should be in view again.

Width 100% hangs out on the right

Now, with one of position: absolute or fixed, set width to 100% by activating the according checkbox. The test element should have full width now, but it hangs out on the right (thus it has more than 100% width).

This is the problem this Blog is about. How can we fix it? Mind that you should not fix it by setting width to something like 96% !


Fix by box-sizing border-box?

Try to set border-width to zero. Now the element fits into its parent. Most likely, because there is no problem when border is zero, we could fix the problem by setting box-sizing to border-box, because then the browser lays it out like paddings and borders were inside the element. Try to do that. You see that the element fits nearly into space now:

But it still hangs out to the right. And it also shows a scrollbar when position is absolute.

Let's say it is a half-fix. Now add a margin of 1 to the element. The half-fix breaks, the element is hanging out to the right again. Reason is that box-sizing: border-box does not include margins.

Fix by attach to right?

Can we fix it by setting the right coordinate, attaching the element to the right bound of its parent container?

To try this out, first reset box-sizing to content-box, and also set margin to zero again. Then set both left and right to zero to attach the elements between the bounds of its layout-parent.

When you are in position: absolute, the browser shows a scrollbar:

In position: fixed, the browser shows no scrollbar:

In both cases the bug is not fixed, not even without margin.

Combine fixes?

Try to set the box-sizing field value to border-box now, while left and right is set to zero:

Looks good. But the final test is setting margin to 1. See what happens:

So this is not a solution! Learning by try & error is hard.

Replace width 100% by left and right zero

Get rid of width: 100%. Leave the work to left and right. Here is the solution that works, and we don't even need box-sizing: border-box for it:

Elements that have position: fixed or position: absolute can be brought to a real 100% width by setting their left and right to zero, and NOT explicitly set width: 100%.