简体   繁体   中英

Why is checking for an attribute using dot notation before removing faster than removing the attribute outright?

I asked this question , and it turned out that when removing an attribute from an element, checking whether the element exists first using elem.xxx!==undefined makes the runtime faster. Proof .

Why is it quicker? There's more code to go through and you'll have to encounter the removeAttribute() method whichever way you go about this.

Well, first thing you need to know is that elem.xxx is not the same as elem.getAttribute() or any other method relative to the attribute.

elem.xxx is a property of a DOM element while attribute and element on the HTML inside the DOM, both are similar but different. For exemple, take this DOM element: <a href="#"> and this code :

//Let say var a is the <a> tag
a.getAttribute('href');// == #
a.href;// == http://www.something.com/# (i.e the complet URL)

But let take a custom attribute : <a custom="test">

//Let say var a is the <a> tag
a.getAttribute('custom');// == test
a.custom;// == undefined

So you can't really compare the speed of both since they don't achieve the same result. But one is clearly faster since properties are a fast access data while attribute use the get/hasAttribute DOM functions.

Now, Why without the condition is faster? Simply because removeAttribute doesn't care is the attribute is missing, it check if it is not.

So using hasAttribute before removeAttribute is like doing the check twice, but the condition is a little slower since it need to check if the condition is satisfied to run the code.

I have a suspicion that the reason for the speed boost are trace trees.

Trace trees were first introduced by Andreas Gal and Michael Franz of the University of California, Irvine, in their paper Incremental Dynamic Code Generation with Trace Trees .

In his blog post Tracing the Web Andreas Gal (the co-author of the paper) explains how tracing Just-in-Time compilers works.

To explain tracing JIT compilers as sententiously as possible (since my knowledge about the subject isn't profound) a tracing JIT compiler does the following:

  1. Initially all the code to be run is interpreted.
  2. A count is kept for the number of times each code path is executed (eg the number of times the true branch of an if statement is executed).
  3. When the number of times a code path is taken is greater than a predefined threshold the code path is compiled into machine code to speed up execution (eg I believe SpiderMonkey executes code paths executed more than once).

Now let's take a look at your code and understand what is causing the speed boost:

Test Case 1: Check

if (elem.hasAttribute("xxx")) {
    elem.removeAttribute("xxx");
}

This code has a code path (ie an if statement). Remember that tracing JITs only optimize code paths and not entire functions. This is what I believe is happening:

  1. Since the code is being benchmarked by JSPerf it's being executed more than once (an understatement). Hence it is compiled into machine code.
  2. However it still incurs the overhead of the extra function call to hasAttribute which is not JIT compiled because it's not a part of the conditional code path (the code between the curly braces).
  3. Hence although the code inside the curly braces is fast the conditional check itself is slow because it's not compiled. It is interpreted. The result is that the code is slow.

Test Case 2: Remove

elem.removeAttribute("xxx");

In this test case we don't have any conditional code paths. Hence the JIT compiler never kicks in. Thus the code is slow.

Test Case 3: Check (Dot Notation)

if (elem.xxx !== undefined) {
    elem.removeAttribute("xxx");
}

This is the same as the first test case with one significant difference:

  1. The conditional check is a simple non-equivalence check. Hence it doesn't incur the full overhead of a function call.
  2. Most JavaScript interpreters optimize simple equivalence checks like this by assuming a fixed data type for both the variables. Since the data type of elem.xxx or undefined is not changing every iteration this optimization makes the conditional check even faster.
  3. The result is that the conditional check (although interpreted) does not slow down the compiled code path significantly. Hence this code is the fastest.

Of course this is just speculation on my part. I don't know the internals of a JavaScript engine and I hence my answer is not canonical. However I opine that it is a good educated guess.

Your proof is incorrect...

elem.class !== undefined always evaluates to false and thus elem.removeAttribute("class") is never called, therefore, this test will always be quicker.

The correct property on elem to use is className , eg:

typeof elem.className !== "undefined"

As Karl-André Gagnon pointed out, accessing a [native] JavaScript property and invoking a DOM function/property are two different operations.

Some DOM properties are exposed as JavaScript properties via the DOM IDL ; these are not the same as adhoc JS properties and require DOM access. Also, even though the DOM properties are exposed, there is not strict relation with DOM attributes !

For instance, inputElm.value = "x" will not update the DOM attribute , even though the element will display and report an updated value. If the goal is to deal with DOM attributes , the only correct method is to use hasAttribute/setAttribute , etc.


I've been working on deriving a "fair" micro-benchmark for the different function calls, but it is fairly hard and there is alot of different optimization that occurs. Here my best result , which I will use to argue my case.

Note that there is no if or removeAttribute to muddle up the results and I am focusing only on the DOM/JS property access. Also, I attempt to rule out the claim that the speed difference is merely due to a function call and I assign the results to avoid blatant browser optimizations. YMMV.

Observations:

  1. Access to a JS property is fast . This is to be expected 1,2

  2. Calling a function can incur a higher cost than direct property access 1 , but is not nearly as slow as DOM properties or DOM functions . That is, it is not merely a "function call" that makes hasAttribute so much slower.

  3. DOM properties access is slower than native JS property access; however, performance differs widely between the DOM properties and browsers. My updated micro-benchmark shows a trend that DOM access - be it via DOM property or DOM function - may be slower than native JS property access 2 .

And going back to the very top: Accessing a non-DOM [JS] property on an element is fundamentally different than accessing a DOM property , much less a DOM attribute , on the same element. It is this fundamental difference, and optimizations (or lack thereof) between the approaches across browsers, that accounts for the observed performance differences.


1 IE 10 does some clever trick where the fake function call is very fast (and I suspect the call has been elided) even though it has abysmal JS property access. However, considering IE an outlier or merely reinforcement that the function call is not what introduces the inherently slower behavior, doesn't detract from my primary argument: it is the DOM access that is fundamentally slower.

2 I would love to say DOM property access is slower, but FireFox does some amazing optimization of input.value (but not img.src ). There is some special magic that happens here. Firefox does not optimize the DOM attribute access.

And, different browsers may exhibit entirely different results .. however, I don't think that one has to consider any "magic" with the if or removeAttribute to at least isolate what I believe to be the "performance issue": actually using the DOM .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM