3

A lot of DOM attributes can be accessed via properties. For example, the class attribute can be accessed via the className property. The answers in the following thread suggest that you should always use properties over attributes wherever possible, for performance reasons.

When to use setAttribute vs .attribute= in JavaScript?

So, I created a helper function which sets properties.

const setAttribute = (node, name, value) => {
    node[name] = value;
};

However, this doesn't work for non-standard attributes such as aria-label which don't have corresponding properties. So, given a node, a name, and a value, how do I determine whether the name is a property name or an attribute name? I'm trying to do something as follows.

const setAttribute = (node, name, value) => {
    if (isPropertyName(node, name, value)) {
        node[name] = value;
    } else {
        node.setAttribute(name, value);
    }
};

Is it possible to implement the isPropertyName function? If so, how? Assume that the attribute wasn't previously created. Hence, node[name] returns undefined and node.hasAttribute(name) returns null.

If there's no way to implement the isPropertyName function, then should I always use setAttribute instead? I need to handle this for the general case. For specific cases, I could create a map from attribute names to property names. Is there a better way to do this?

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • 1
    `always use properties over attributes wherever possible, for performance reasons.` Might be worth pointing out that wrapping such functionality into a function may negate any performance advantages you might get. Might be worth bench testing any solution to see if this is the case. – Keith Aug 13 '19 at 10:08

3 Answers3

4

The answers in the following thread suggest that you should always use properties over attributes wherever possible, for performance reasons.

This smells like premature optimisation. Is the setting of properties/attributes something that happens so frequently in your application that the wrong choice is among your biggest performance problems?

So, given a node, a name, and a value, how do I determine whether the name is a property name or an attribute name?

You would need to manually construct a loop-up table. There's no way to reliably determine this programmatically.

Case in point, an input element object will have a value attribute which maps onto the defaultValue property but also has a separate value property.

Meanwhile, the value of an onclick attribute must be a string while the value of the onclick property must be a function.

Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335
  • I see what you mean, but I don't really see the problem. If a name can be both a property name and an attribute name then we'd always prioritize using the property name. – Aadit M Shah Aug 13 '19 at 10:16
  • @AaditMShah — Then you'd assign a string to `onclick` which wouldn't work. Or you'd set the current value instead of the default value, which would have different behaviour. – Quentin Aug 13 '19 at 10:17
  • Hmm... perhaps my question wasn't specific enough. The type of `setAttribute` would be `(Node, String, Value) -> Void`. The value could be a non-string value in which case you could write `setAttribute(button, "onclick", () => alert("clicked"))`. In this case, we'd prioritize using the `onclick` property name. Hence, things would work out fine. If the user of the function passes in a string instead then that's their fault. By default, we prioritize property names over attribute names. – Aadit M Shah Aug 13 '19 at 10:21
  • Besides, it's widely considered bad practice to use strings to event handlers. Eval is considered evil. – Aadit M Shah Aug 13 '19 at 10:22
1

If the properties you want to change are always either properties which change attributes (like className) or attributes themselves (like class), if you want to differentiate between them, you can identify whether the passed string corresponds to a settable property by checking for a setter on the node. (All properties which change the underlying HTML as a side-effect, like className, id, etc, are setters.)

const propertyExists = (node, name) => name in node;

console.log(propertyExists(div, 'className'));
console.log(propertyExists(div, 'class'));
<div id="div"></div>

const propertyInfo = (node, name) => {
  let obj = node;
  let info;
  while (!info && obj) {
    info = Object.getOwnPropertyDescriptor(obj, name);
    obj = Object.getPrototypeOf(obj);
  }
  return info;
};

console.log(propertyInfo(div, 'className'));
console.log(propertyInfo(div, 'class'));
<div id="div"></div>

If a property is found, it'll be a getter/setter. If no such property is found, use setAttribute instead:

const setAttribute = (node, name, value) => {
    if (name in node) {
        node[name] = value;
    } else {
        node.setAttribute(name, value);
    }
};
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    This is such an obvious solution. For some reason I thought that attribute properties were some magic browser implementation. Didn't think of using the `in` operator. Thank you. – Aadit M Shah Aug 13 '19 at 10:07
  • @AaditMShah Worth pointing out the performance aspect, seen as it's mentioned in your question. Doing some quick benchmarks, using this function on `node.class = ` will make this 50% slower than directly accessing it. Using this on properties has little effect on performance. Using a lookup table like @Quentin mentioned keeps things fast. – Keith Aug 13 '19 at 11:08
  • @Keith It would be helpful if you could share your benchmark. Perhaps create a jsperf? Also, you wouldn't write `node.class = value`. You'd write either `node.className = value` or `node.setAttribute("class", value)`. – Aadit M Shah Aug 13 '19 at 11:49
1

Seen as performance was mentioned in the original question, I thought I would knock up a quick little benchmark snippet.

In Chrome / Windows, these were my results.

| method                            | ms   | % slower |
|-----------------------------------|------|----------|
| lookupFn.className(div, value)    | 692  | 0.0      |
| node.className = value            | 706  | 2.0      |
| setAttribute('className', testval)| 958  | 27.8     |
| node.setAttribute('class',value)  | 1431 | 51.6     |
| lookupFn.class(node, value)       | 1449 | 52.2     |
| setAttribute(node, 'class', value)| 1796 | 61.5     |

Interestingly, using a lookup was often faster than even doing node.prop =

Side note: Just tried this in Firefox, and the numbers were nearly twice as fast. Also tried edge, and eh!!. Well all setAttribute variations were about 10 times slower, but property access was about the same.

const loops = 50000;

const div = document.querySelector("div");


const lookupFn = {
  class: (node, value) => node.setAttribute('class', value),
  className: (node, value) => node.className = value
}

const setAttribute = (node, name, value) => {
    if (name in node) {
        node[name] = value;
    } else {
        node.setAttribute(name, value);
    }
};

const testval = "testval";

const tests = [
  {
    name: "node.setAttribute('class',value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        div.setAttribute('class', testval);    
      }
    }
  }, {
    name: "setAttribute(node, 'class', value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        setAttribute(div, 'class', testval);    
      }
    }
  }, {
    name: "lookupFn.class(node, value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        lookupFn.class(div, testval);    
      }
    }
  }, {
    name: "'class' in div",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        if ('class' in div) {
          div['class'] = testval;
        } else {
          div.setAttribute('class', testval);
        }
      }
    }
  }, {
    name: "node.className = value",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        div.className = testval;    
      }
    }
  }, {
    name: "setAttribute('className', testval)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        setAttribute(div, 'className', testval);    
      }
    }
  }, {
    name: "lookupFn.className(div, value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        lookupFn.className(div, testval);    
      }
    }
  }, {
    name: "'className' in div",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        if ('className' in div) {
          div['className'] = testval;
        } else {
          div.setAttribute('className', testval);
        }
      }
    }
  }
];

for (const test of tests) test.ms = 0;

function runTests() {
  for (let outer = 0; outer < 100; outer += 1) {
    for (const test of tests) {
      const st = Date.now();
      const {fn, name} = test;
      fn();
      test.ms += Date.now() - st;
    }
  } 
}

function showResults() {
  tests.sort((a, b) => a.ms - b.ms);
  const fastest = tests[0];
  const tbody = document.querySelector("tbody");
  for (const test of tests) {
    const diff = (test.ms-fastest.ms)*100/fastest.ms;
    const tr = document.createElement("tr");
    let td = document.createElement("td");
    td.innerText = test.name;
    tr.appendChild(td);
    td = document.createElement("td");
    td.innerText = test.ms;
    tr.appendChild(td);
    td = document.createElement("td");
    td.innerText = diff.toFixed(1);
    tr.appendChild(td);
    tbody.appendChild(tr);
  }
}

div.innerText = "Please Wait..";
setTimeout(() => {
  runTests();
  showResults();
  div.innterText = "Results..";
}, 100);
table { width: 100% }
<div></div>

<table border="1">
  <thead>
    <tr>
      <th>Method</th>
      <th>Ms</th>
      <th>% slower</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
Keith
  • 22,005
  • 2
  • 27
  • 44
  • Thanks for this benchmark. I really appreciate the effort you put into it. I had a feeling that your results were biased. Hence, I took the liberty of editing your answer to add more test cases. My new test cases use the `in` operator directly without using an extra function call. Turns out, using the `in` operator is on par in terms of performance with the lookup table approach. In addition, it's more general. The slowdown was due to the extra function call. Furthermore, this benchmark also confirms that properties are indeed faster than attributes. Finally, I fixed your % slower formula. 0=) – Aadit M Shah Aug 13 '19 at 17:43
  • You should probably update your answer with your new results. – Aadit M Shah Aug 13 '19 at 17:46