青岛刘新 新浪个人认证
  • 博客等级:
  • 博客积分:0
  • 博客访问:249,334
  • 关注人气:127
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

[翻译]High Performance JavaScript(011)

(2010-04-30 00:00:00)




分类: 软件技术

Repaints and Reflows  重绘和重排版


    Once the browser has downloaded all the components of a page—HTML markup, JavaScript, CSS, images—it parses through the files and creates two internal data structures:



A DOM tree
    A representation of the page structure




A render tree
    A representation of how the DOM nodes will be displayed




    The render tree has at least one node for every node of the DOM tree that needs to be displayed (hidden DOM elements don't have a corresponding node in the render tree). Nodes in the render tree are called frames or boxes in accordance with the CSS model that treats page elements as boxes with padding, margins, borders, and position. Once the DOM and the render trees are constructed, the browser can display ("paint") the elements on the page.



    When a DOM change affects the geometry of an element (width and height)—such as a change in the thickness of the border or adding more text to a paragraph, resulting in an additional line—the browser needs to recalculate the geometry of the element as well as the geometry and position of other elements that could have been affected by the change. The browser invalidates the part of the render tree that was affected by the change and reconstructs the render tree. This process is known as a reflow. Once the reflow is complete, the browser redraws the affected parts of the screen in a process called repaint.



    Not all DOM changes affect the geometry. For example, changing the background color of an element won't change its width or height. In this case, there is a repaint only (no reflow), because the layout of the element hasn't changed.



    Repaints and reflows are expensive operations and can make the UI of a web application less responsive. As such, it's important to reduce their occurrences whenever possible.



When Does a Reflow Happen?  重排版时会发生什么?


    As mentioned earlier, a reflow is needed whenever layout and geometry change. This happens when:


• Visible DOM elements are added or removed


• Elements change position


• Elements change size (because of a change in margin, padding, border thickness, width, height, etc.)


• Content is changed, e.g., text changes or an image is replaced with one of a different size


• Page renders initially


• Browser window is resized


    Depending on the nature of the change, a smaller or bigger part of the render tree needs to be recalculated. Some changes may cause a reflow of the whole page: for example, when a scroll bar appears.



Queuing and Flushing Render Tree Changes  查询并刷新渲染树改变


    Because of the computation costs associated with each reflow, most browsers optimize the reflow process by queuing changes and performing them in batches. However, you may (often involuntarily) force the queue to be flushed and require that all scheduled changes be applied right away. Flushing the queue happens when you want to retrieve layout information, which means using any of the following:



offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)(在IE中此函数称为currentStyle


    The layout information returned by these properties and methods needs to be up to date, and so the browser has to execute the pending changes in the rendering queue and reflow in order to return the correct values.



    During the process of changing styles, it's best not to use any of the properties shown in the preceding list. All of these will flush the render queue, even in cases where you're retrieving layout information that wasn't recently changed or isn't even relevant to the latest changes.



    Consider the following example of changing the same style property three times (this is probably not something you'll see in real code, but is an isolated illustration of an important topic):



// setting and retrieving styles in succession
var computed,
    tmp = '',
    bodystyle = document.body.style;
if (document.body.currentStyle) { // IE, Opera
  computed = document.body.currentStyle;
} else { // W3C
  computed = document.defaultView.getComputedStyle(document.body, '');
// inefficient way of modifying the same property
// and retrieving style information right after

bodystyle.color = 'red';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
tmp = computed.backgroundAttachment;

    In this example, the foreground color of the body element is being changed three times, and after every change, a computed style property is retrieved. The retrieved properties—backgroundColor, backgroundImage, and backgroundAttachment—are unrelated to the color being changed. Yet the browser needs to flush the render queue and reflow due to the fact that a computed style property was requested.

    在这个例子中,body元素的前景色被改变了三次,每次改变之后,都导入computed的风格。导入的属性backgroundColor, backgroundImage, 和backgroundAttachment与颜色改变无关。然而,浏览器需要刷新渲染队列并重排版,因为computed的风格被查询而引发。


    A better approach than this inefficient example is to never request layout information while it's being changed. If the computed style retrieval is moved to the end, the code looks like this:



bodystyle.color = 'red';
bodystyle.color = 'white';
bodystyle.color = 'green';
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;

    The second example will be faster across all browsers, as seen in Figure 3-7.



[翻译]High <wbr>Performance <wbr>JavaScript(011)

Figure 3-7. Benefit of preventing reflows by delaying access to layout information

图3-7  通过延迟访问布局信息避免重排版而带来的性能提升


Minimizing Repaints and Reflows  最小化重绘和重排版


    Reflows and repaints can be expensive, and therefore a good strategy for responsive applications is to reduce their number. In order to minimize this number, you should combine multiple DOM and style changes into a batch and apply them once.



Style changes  改变风格


    Consider this example:



var el = document.getElementByIdx('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';


    Here there are three style properties being changed, each of them affecting the geometry of the element. In the worst case, this will cause the browser to reflow three times. Most modern browsers optimize for such cases and reflow only once, but it can still be inefficient in older browsers or if there's a separate asynchronous process happening at the same time (i.e., using a timer). If other code is requesting layout information while this code is running, it could cause up to three reflows. Also, the code is touching the DOM four times and can be optimized.



    A more efficient way to achieve the same result is to combine all the changes and apply them at once, modifying the DOM only once. This can be done using the cssText property:



var el = document.getElementByIdx('mydiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

    Modifying the cssText property as shown in the example overwrites existing style information, so if you want to keep the existing styles, you can append this to the cssText string:



el.style.cssText += '; border-left: 1px;';

    Another way to apply style changes only once is to change the CSS class name instead of changing the inline styles. This approach is applicable in cases when the styles do not depend on runtime logic and calculations. Changing the CSS class name is cleaner and more maintainable; it helps keep your scripts free of presentation code, although it might come with a slight performance hit because the cascade needs to be checked when changing classes.



var el = document.getElementByIdx('mydiv');
el.className = 'active';


Batching DOM changes  批量修改DOM


    When you have a number of changes to apply to a DOM element, you can reduce the number of repaints and reflows by following these steps:



1. Take the element off of the document flow.


2. Apply multiple changes.


3. Bring the element back to the document.



    This process causes two reflows—one at step 1 and one at step 3. If you omit those steps, every change you make in step 2 could cause its own reflows.



    There are three basic ways to modify the DOM off the document:



• Hide the element, apply changes, and show it again.


• Use a document fragment to build a subtree outside of the live DOM and then copy it to the document.


• Copy the original element into an off-document node, modify the copy, and then replace the original element once you're done.



    To illustrate the off-document manipulations, consider a list of links that must be updated with more information:



<ul id="mylist">
  <li><a href="http://phpied.com">Stoyan</a></li>
  <li><a href="http://julienlecomte.com">Julien</a></li>

    Suppose additional data, already contained in an object, needs to be inserted into this list. The data is defined as:



var data = [
    "name": "Nicholas",
    "url":  "
    "name": "Ross",
    "url":  "

    The following is a generic function to update a given node with new data:



function appendDataToElement(appendToElement, data) {
  var a, li;
  for (var i = 0, max = data.length; i < max; i++) {
    a = document.createElement_x('a');
    a.href = data[i].url;
    li = document.createElement_x('li');

    The most obvious way to update the list with the data without worrying about reflows would be the following:



var ul = document.getElementByIdx('mylist');
appendDataToElement(ul, data);

    Using this approach, however, every new entry from the data array will be appended to the live DOM tree and cause a reflow. As discussed previously, one way to reduce reflows is to temporarily remove the <ul> element from the document flow by changing the display property and then revert it:



var ul = document.getElementByIdx('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

    Another way to minimize the number of reflows is to create and update a document fragment, completely off the document, and then append it to the original list. A document fragment is a lightweight version of the document object, and it's designed to help with exactly this type of task—updating and moving nodes around. One syntactically convenient feature of the document fragments is that when you append a fragment to a node, the fragment's children actually get appended, not the fragment itself. The following solution takes one less line of code, causes only one reflow, and touches the live DOM only once:



var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);

    A third solution would be to create a copy of the node you want to update, work on the copy, and then, once you're done, replace the old node with the newly updated copy:



var old = document.getElementByIdx('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);

    The recommendation is to use document fragments (the second solution) whenever possible because they involve the least amount of DOM manipulations and reflows. The only potential drawback is that the practice of using document fragments is currently underused and some team members may not be familiar with the technique.



Caching Layout Information  缓冲布局信息


    As already mentioned, browsers try to minimize the number of reflows by queuing changes and executing them in batches. But when you request layout information such as offsets, scroll values, or computed style values, the browser flushes the queue and applies all the changes in order to return the updated value. It is best to minimize the number of requests for layout information, and when you do request it, assign it to local variables and work with the local values.



    Consider an example of moving an element myElement diagonally, one pixel at a time, starting from position 100 × 100px and ending at 500 × 500px. In the body of a timeout loop you could use:



// inefficient
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {

    This is not efficient, though, because every time the element moves, the code requests the offset values, causing the browser to flush the rendering queue and not benefit from its optimizations. A better way to do the same thing is to take the start value position once and assign it to a variable such as var current = myElement.offsetLeft;. Then, inside of the animation loop, work with the current variable and don't request offsets:

    这样做很没效率,因为每次元素移动,代码查询偏移量,导致浏览器刷新渲染队列,并没有从优化中获益。另一个办法只需要获得起始位置值一次,将它存入局部变量中var current = myElement.offsetLeft;。然后,在动画循环中,使用current变量而不再查询偏移量:


myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (current >= 500) {


Take Elements Out of the Flow for Animations  将元素提出动画流


    Showing and hiding parts of a page in an expand/collapse manner is a common interaction pattern. It often includes geometry animation of the area being expanded, which pushes down the rest of the content on the page.



    Reflows sometimes affect only a small part of the render tree, but they can affect a larger portion, or even the whole tree. The less the browser needs to reflow, the more responsive your application will be. So when an animation at the top of the page pushes down almost the whole page, this will cause a big reflow and can be expensive, appearing choppy to the user. The more nodes in the render tree that need recalculation, the worse it becomes.



    A technique to avoid a reflow of a big part of the page is to use the following steps:



1. Use absolute positioning for the element you want to animate on the page, taking it out of the layout flow of the page.


2. Animate the element. When it expands, it will temporarily cover part of the page. This is a repaint, but only of a small part of the page instead of a reflow and repaint of a big page chunk.


3. When the animation is done, restore the positioning, thereby pushing down the rest of the document only once.








IE and :hover  IE和:hover


    Since version 7, IE can apply the :hover CSS pseudo-selector on any element (in strict mode). However, if you have a significant number of elements with a :hover, the responsiveness degrades. The problem is even more visible in IE 8.



    For example, if you create a table with 500–1000 rows and 5 columns and use tr:hover to change the background color and highlight the row the user is on, the performance degrades as the user moves over the table. The highlight is slow to apply, and the CPU usage increases to 80%–90%. So avoid this effect when you work with a large number of elements, such as big tables or long item lists.



Event Delegation  事件托管


    When there are a large number of elements on a page and each of them has one or more event handlers attached (such as onclick), this may affect performance. Attaching every handler comes at a price—either in the form of heavier pages (more markup or JavaScript code) or in the form of runtime execution time. The more DOM nodes you need to touch and modify, the slower your application, especially because the event attaching phase usually happens at the onload (or DOMContentReady) event, which is a busy time for every interaction-rich web page. Attaching events takes processing time, and, in addition, the browser needs to keep track of each handler, which takes up memory. And at the end of it, a great number of these event handlers might never be needed(because the user clicked one button or link, not all 100 of them, for example), so a lot of the work might not be necessary.



    A simple and elegant technique for handling DOM events is event delegation. It's based on the fact that events bubble up and can be handled by a parent element. With event delegation, you attach only one handler on a wrapper element to handle all events that happen to the children descendant of that parent wrapper.



    According to the DOM standard, each event has three phases:



• Capturing

• At target

• Bubbling



    Capturing is not supported by IE, but bubbling is good enough for the purposes of delegation. Consider a page with the structure shown in Figure 3-8.



[翻译]High <wbr>Performance <wbr>JavaScript(011)

Figure 3-8. An example DOM tree

图3-8  一个DOM树的例子


    When the user clicks the "menu #1" link, the click event is first received by the <a> element. Then it bubbles up the DOM tree and is received by the <li> element, then the <ul>, then the <div>, and so on, all the way to the top of the document and even the window. This allows you to attach only one event handler to a parent element and receive notifications for all events that happen to the children.

    当用户点击了“menu #1”链接,点击事件首先被<a>元素收到。然后它沿着DOM树冒泡,被<li>元素收到,然后是<ul>,接着是<div>,等等,一直到达文档的顶层,甚至window。这使得你可以只在父元素上挂接一个事件句柄,来接收所有子元素产生的事件通知。


    Suppose that you want to provide a progressively enhanced Ajax experience for the document shown in the figure. If the user has JavaScript turned off, then the links in the menu work normally and reload the page. But if JavaScript is on and the user agent is capable enough, you want to intercept all clicks, prevent the default behavior (which is to follow the link), send an Ajax request to get the content, and update a portion of the page without a refresh. To do this using event delegation, you can attach a click listener to the UL "menu" element that wraps all links and inspect all clicks to see whether they come from a link.



document.getElementByIdx('menu').onclick = function(e) {
  // x-browser target
  e = e || window.event;
  var target = e.target || e.srcElement;
  var pageid, hrefparts;
  // only interesed in hrefs
  // exit the function on non-link clicks
  if (target.nodeName !== 'A') {
  // figure out page ID from the link
  hrefparts = target.href.split('/');
  pageid = hrefparts[hrefparts.length - 1];
  pageid = pageid.replace('.html', '');
  // update the page
  ajaxRequest('xhr.php?page=' + id, updatePageContents);
  // x-browser prevent default action and cancel bubbling
  if (typeof e.preventDefault === 'function') {
  } else {
    e.returnValue = false;
    e.cancelBubble = true;

    As you can see, the event delegation technique is not complicated; you only need to inspect events to see whether they come from elements you're interested in. There's a little bit of verbose cross-browser code, but if you move this part to a reusable library, the code becomes pretty clean. The cross-browser parts are:



• Access to the event object and identifying the source (target) of the event


• Cancel the bubbling up the document tree (optional)


• Prevent the default action (optional, but needed in this case because the task was to trap the links and not follow them)



Summary  总结


    DOM access and manipulation are an important part of modern web applications. But every time you cross the bridge from ECMAScript to DOM-land, it comes at a cost. To reduce the performance costs related to DOM scripting, keep the following in mind:



• Minimize DOM access, and try to work as much as possible in JavaScript.


• Use local variables to store DOM references you'll access repeatedly.


• Be careful when dealing with HTML collections because they represent the live, underlying document. Cache the collection length into a variable and use it when iterating, and make a copy of the collection into an array for heavy work on collections.


• Use faster APIs when available, such as querySelectorAll() and firstElementChild.


• Be mindful of repaints and reflows; batch style changes, manipulate the DOM tree "offline," and cache and minimize access to layout information.


• Position absolutely during animations, and use drag and drop proxies.


• Use event delegation to minimize the number of event handlers.



阅读 评论 收藏 转载 喜欢 打印举报/Report
  • 评论加载中,请稍候...




    新浪BLOG意见反馈留言板 电话:4000520066 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有