显示标签为“Let's make the web faster”的博文。显示所有博文
显示标签为“Let's make the web faster”的博文。显示所有博文
2009年10月2日星期五

[翻译]加速 JavaScript:处理 DOM

当制作 RIA(Rich Internet Application) 时,我们使用 JavaScript 来改变或增加元素。这是靠 DOM (Document Object Model 文档对象模型) 来完成的,这是如何影响 app 的速度呢?

处理 DOM 会使浏览器 reflow (浏览器决定如何显示东东的进程)。直接操作 DOM,改变元素的 CSS 样式,改变浏览器窗口的大小会触发 reflow。访问元素的布局属性(如 offsetHeight 和 offsetWidth) 会触发 reflow。由于每次 reflow 需要时间,所有我们能减少 reflow 越多,app 越快。

处理 DOM 不仅操作页面中存在的元素还包括创建的元素。下面四个部分覆盖了 DOM 操作和 DOM generation,减少浏览器中 reflow 触发的次数。

CSS class 切换 DOM 操作

这个部分我们改变一个元素和它后代的多个样式属性,只触发一次回流。

问题

我们写一个函数来改变一个元素中的所有链接的 className 属性。可以通过简单遍历每个 a 并更新他们的 className 属性。

function selectAnchor(element) {
  element.style.fontWeight = 'bold';
  element.style.textDecoration = 'none';
  element.style.color = '#000';
}

解决方法

我们可以增加一个设置所有样式属性的 class 来解决这个问题。通过给元素增加这个class,现在我们只触发一次浏览器 reflow。同时分离了表现和行为(译注:这个在我之前的《body标签class属性的妙用(Google Reader前端简单分析)》中也有提及)

.selectedAnchor {
  font-weight: bold;
  text-decoration: none;
  color: #000;
}

function selectAnchor(element) {
  element.className = 'selectedAnchor';
}

Out-of-the-flow DOM 操作

这个部分我们创建多个元素并把它们插入 DOM 只触发一次 reflow。使用了叫做 DocumentFragment 的东东。我们在 DOM 外创建一个 DocumentFragment (所以是 out-of-the-flow)。之后创建并添加进多个元素。最后,我们移动该 DocumentFragment  中的所有元素到 DOM,这只触发一次 reflow。

问题

让我们写一个改变一个元素中所遇链接的 className 属性的函数。通过简单的遍历每个 a 并更新他们的 href 属性。问题在于,每个 a 会触发一次 reflow。

function updateAllAnchors(element, anchorClass) {
  var anchors = element.getElementsByTagName('a');
  for (var i = 0, length = anchors.length; i < length; i ++) {
    anchors[i].className = anchorClass;
  }
}

解决方法

解决这个问题,可以从 DOM 中移除元素,更新所有链接,之后插入回元素到原始位置。为此,我们写一个可复用的函数,不仅从 DOM中移除元素,并且返回一个将会插入回元素至元素位置的函数。

/**
 * Remove an element and provide a function that inserts it into its original position
 * @param element {Element} The element to be temporarily removed
 * @return {Function} A function that inserts the element into its original position
 **/
function removeToInsertLater(element) {
  var parentNode = element.parentNode;
  var nextSibling = element.nextSibling;
  parentNode.removeChild(element);
  return function() {
    if (nextSibling) {
      parentNode.insertBefore(element, nextSibling);
    } else {
      parentNode.appendChild(element);
    }
  };
}

现在我们用这个函数去更新一个元素中的所有链接,这 out-of-the-flow,并且在移除元素时和插入元素时只触发一次 reflow。

function updateAllAnchors(element, anchorClass) {
  var insertFunction = removeToInsertLater(element);
  var anchors = element.getElementsByTagName('a');
  for (var i = 0, length = anchors.length; i < length; i ++) {
    anchors[i].className = anchorClass;
  }
  insertFunction();
}

单个元素的 DOM Generation

这个部分我们创建并添加一个元素至 DOM 中,仅触发一次 reflow。创建元素后,在新元素添加至 DOM 前做好所有的改变。

问题

写一个添加新a 元素至父元素的函数。这个函数让你为该链接提供 class 和 文本。我们可以创建元素,添加进 DOM,之后设置这些属性。这触发3次 reflow。

function addAnchor(parentElement, anchorText, anchorClass) {
  var element = document.createElement('a');
  parentElement.appendChild(element);
  element.innerHTML = anchorText;
  element.className = anchorClass;
}

解决方法

最后插入这个子节点至 DOM。这触发一次 reflow。

function addAnchor(parentElement, anchorText, anchorClass) {
  var element = document.createElement('a');
  element.innerHTML = anchorText;
  element.className = anchorClass;
  parentElement.appendChild(element);
}

尽管如此,如果我们要添加大量的链接至一个元素时这有一些问题。通过这个方法,每添加一个链接触发一次 reflow。下一部分解决这个问题。

DocumentFragment DOM Generation

这个部分,我们创建多个元素并把它们插入 DOM 触发一次 reflow。使用了一个叫做 DocumentFragment 的东东。我们在 DOM 外创建一个 DocumentFragment (所以是 out-of-the-flow)。之后创建并添加多个元素进来。最后,把所有 DocumentFragment 中的元素移至 DOM 并只触发一次 reflow。

问题

写一个添加10个连接至一个元素的函数。如果只是简单的添加每个新链接至这个元素,会触发10次 reflow。

function addAnchors(element) {
  var anchor;
  for (var i = 0; i < 10; i ++) {
    anchor = document.createElement('a');
    anchor.innerHTML = 'test';
    element.appendChild(anchor);
  }
}

解决方法

为了解决这个问题,我们创建一个 DocumentFragment 并添加每个新链接至此。当我们使用appendChild 添加 DocumentFragment 至元素时,所有 DocumentFragment 的子节点 实际上添加进了这个元素中。这只触发一次 reflow。

function addAnchors(element) {
  var anchor, fragment = document.createDocumentFragment();
  for (var i = 0; i < 10; i ++) {
    anchor = document.createElement('a');
    anchor.innerHTML = 'test';
    fragment.appendChild(anchor);
  }
  element.appendChild(fragment);
}

[翻译]用Page Speed Activity捕获并分析浏览器渲染

安装

Page Speed 是一个 Firebug/Firefox 扩展,安装地址:http://code.google.com/speed/page-speed/download.html

背景:逐步呈现

快速的网页逐步呈现。即随浏览器的加载而逐步显示其内容。一个逐步呈现的页面给浏览者页面在加载的视觉回馈,并尽快给给用户请求信息。Google Yahoo 都建议逐步呈现页面,比如把 CSS 写在页面 head 中。

对多绝大多数页面来说,有一些很好的实践来优化逐步呈现。一个快速的页面应该先给用户呈现可见的内容(译注:浏览器第一屏),随后呈现视线外的内容(即当前滚动区域外的内容)。一个快速的页面会在加载和渲染重量级的资源(如图片和视频)之前加载和渲染轻量级的资源(如文字)。

一些众所周知的技术会禁止逐步呈现。使用大量表格,甚至用来布局,在一些浏览器中会使逐步呈现失效。应用样式表在页面后方,即便这些样式表在开始的页面加载中使用不到,也会阻止逐步呈现。

用Page Speed Activity捕获浏览器渲染

很难决定一个页面是否要进行逐步呈现优化。大多数页面对肉眼来说呈现太快,以至于意识不到个别的渲染事件(尤其是在高速网络连接下),而且看不到屏幕区域外的内容是否呈现了。

幸运的是,Firefox 3.5 支持捕获浏览器渲染事件。Page Speed Activity 面板用这个功能可为页面呈现过程录制一个“幻灯片”。幻灯片的每一单元显示了哪一个屏幕区域被渲染(黄色),哪些区域在屏幕外(灰色)用户看不到。

启用渲染快照

由于捕获渲染快照会增加一些开销并拖慢浏览器,Page Speed Activity 屏幕快照默认是禁用的。启用渲染快照,确保Activity 下拉菜单中的 "Paint Snapshots (slow)" 被选中。

Activity 面板中的渲染快照

一旦渲染快照启用,就开始用 Activity 面板录制。Activity 面板将在时间线上显示网络,缓存,和 JavaScript 事件。渲染快照幻灯片出在 Activity 面板右侧。渲染快照会按捕获顺序绘制,最早的快照在顶部。拖动右侧的滚动条可看到所有的快照。

快照回放示例

译注:请到这里看演示

在这个例子中,我们可以看到以渲染快照慢动作回放的 Google 搜索结果页面的逐步呈现。这些快照用 modem 连接捕获。

用 Page Speed Activity 回放这个例子的渲染快照,请点击 Play Paint Events按钮。

在快照中,用户可见的区域为白色,用户不看见的区域(当前浏览器滚动区域之外)用灰色遮罩。每个快照用黄色遮罩显示重绘区域。

请注意,可见区域的文本内容首先呈现,之后是屏幕外的文本内容。通过先呈现屏幕内的可见区域,用户尽早获取了尽可能多的有用信息。

文本内容呈现后,图像内容接着呈现。推迟图像内容呈现直到文本内容下载完成并呈现完成,这使得浏览器尽早的显示文本内容,再一次尽早给用户尽可能多的有用信息。

在页面标签中给所有图像指定宽度和高度属性,浏览器不需在图像加载后 reflow 页面。尽管没有严格的与逐步呈现有关,指定图像的宽高会带来更好的用户体验,在页面加载时页面内容不会在附近移动。

最后,注意图像自身的逐渐呈现。允许用户在全部图像完成加载前看到图像内容。现代浏览器在 HTML 中用 <img> 标签逐步呈现图像。相反,许多浏览器中用 CSS 的background-image 属性不会逐步呈现。为了逐步呈现图像,用 HTML 的 <img> 标签来代替 CSS 的 background-image 属性。


总结

使用 Page Speed Activity 面板的渲染快照功能,我们可以清晰的观察页面的逐步呈现行为。这使得网页开发者可以为逐步呈现而优化页面,这是快速网页非常重要的特性之一。

延伸阅读

用 HTML 5 来提升性能

Google 网站管理员 Jens Meiert 发表了一篇《Using HTML 5 for performance improvements》,大部分内容在《Reducing the file size of HTML documents》(中文版)中都有提及,新增的部分为async (和 defer),以下为该段的翻译:

asyncdefer 使用在 script 元素上。

要解释为什么这些属性会提升性能,最好是看一下未使用时发生了什么 -- 各自的脚本在用户代理(浏览器,下同)继续解析页面前将被提取并直接执行,有时这个行为是我们想要的,有时不是。

新的 async 属性允许各自的脚本在可用时异步执行。HTML 4 中已经包含 defer 属性,指出 defer “对用户代理提供一个暗示:脚本不产生任何文档内容(比如,在 Javascript 中没有 document.write ),这样,用户代理会继续解析并渲染”。

如果不使用 async 属性而只使用 defer,脚本在页面解析完成时已经执行。即使指定了 async 属性,也可同时指定defer 属性。这使得那些旧的只理解 defer 的浏览器来回退 defer 行为而非默认的同步阻塞行为。

2009年7月24日星期五

[翻译] gzip 压缩原理

译注:本打算翻译这篇文章的,搜了一下已经有人翻译过了,不过网站失效,通过快照获取了部分内容,在此基础之上进行了一些修改,感谢 bloglei

译注:以上是 youtube 视频,请翻墙观看(文中图片调用自 picasa,若显示不了也要翻墙)

浏览器请求有无 gzip 压缩的高级预览

服务器未 gzip:

浏览器:

  • 连接服务器并请求页面。
  • 通知服务器,浏览器支持 gzip “Accept-Encoding: gzip”。

服务器:

  • 不支持 gzip。忽略 gzip 请求。发送未压缩的页面。

浏览器:

  • 接收页面。
  • 显示页面。

服务器 gzip:

浏览器:

  • 连接服务器。
  • 通知服务器,浏览器支持 gzip “Accept-Encoding: gzip”。

服务器:

  • 收到 gzip 支持通知。
  • 发送 gzip 编码过的页面,并设置响应头“Content-Encoding: gzip”。

浏览器:

  • 接收页面。
  • 根据响应头“Content-Encoding: gzip” 解码 gzip 编码过的页面。
  • 显示页面。

如何进行 gzip 压缩

简单的说,gzip 压缩是在一个文本文件中找出类似的字符串,并临时替换他们,使整个文件变小。这种形式的压缩对 web 来说特别适合,因为 HTML 和 CSS 文件通常包含大量的重复字符串,例如空格,标签,及样式定义。

例子

在这个例子中,我将用一小段代码演示 gzip 压缩中用相同的标签跟不同的标签的对比。

在第一段代码中,我用了5个标题标签。

未压缩: 69 bytes

压缩后: 85 bytes (译注:某天77问我怎么压缩后比压缩前还大?我回来后看了下原文并测试了一下,事实如此,并非翻译错)

<h1>One</h1>
<h2>Two</h2>
<h3>Three</h3>
<h4>Four</h4>
<h5>Five</h5>

把标题标签改为相同的 div 标签,源代码增大了 10 bytes,但是压缩后,它缩小到 66 bytes,比先前的源代码片段还小!

未压缩: 79字节

压缩后: 66 字节

<div>One</div>
<div>Two</div>
<div>Three</div>
<div>Four</div>
<div>Five</div>

许多开发者可能依此只使用 div 和 span 标签而使页面更大程度的压缩。在多数情况下当然无可非议,但必须注意到正确的语义化标记对可访问性(accessibility)来说非常重要,而且使页面更容易阅读。即便如此,可以使用此方法做一些优化,这取决于你,开发者来决定哪些是合适的。

延伸阅读

2009年6月30日星期二

[翻译]界面提示与感知延迟

对一般的用户而言,速度并不意味着性能。用户对网站速度的感知很大程度上取决于其整体感受,包括他们如何高效的从网站获取所需,以及网站反映出来的易响应性。

当设计 Web 站点或 Web 应用时,谨记用户来你的网站是有目的的。他们能越快(越容易)达到他们的访问目的越好。如果用户在获取内容时遇到了许多困难,他们将会离开你的网站,而去能让他们更快达成目标的其他网站。

节省用户时间,让他们感觉没那么慢有很多工作可以做,本文只涉及提示信息。

提示信息,要考虑三点:

  1. 网站是否足够简单和直观,首次访问者是否可以很容易上手?

    如果不是,花些时间去设计首次运行经验(first-run-experience)提示。

    比方说,你的网站是一个拥有数项功能的强大的 Web 应用。鉴于为此类型的应用程序设计一套直观的“开箱即用”(out-of-the-box )体验并不是一项容易的事情,用户可能需要一个入门帮助。

    首次运行经验简单的为用户说明产品是什么以及/或如何使用其亮点。从长远上看,花一点点时间让用户提前了解产品的一些关键点会给他们节省大量时间。

    提示:不要过头!不要阻止用户获取实际内容,这会令首次运行经验变成额外的一步(a cumbersome multi-step process)。

  2. 提示信息是否会中断或增加用户操作?

    仔细考虑如何显示信息会增加用户的操作流程。在不影响用户操作的情况下可能有更合适的方式去显示提示信息。

    想象一个场景,用户完成一个动作,你认为很完美的。你认为需要二次确认(double-checking)的,为了防止用户意外出错。所以你弹出一个提示“你确定这样做吗?”,这阻止了用户的一个大的错误操作,但对于确定要执行这个动作的用户来说,你增加了一个额外的操作。作为替代,你可以运行操作立即执行,同时在操作后增加一个撤消的功能。

  3. 等待时如何让用户静心(reassure)

    面对现实吧,有些时候用户不得不等待。然而,你可以做一些工作使不可避免的等待时间变得比较好度过(bearable)。

    如果用户等待的时间较长,使用进度条。进度条不只表明必须等待,同时粗略的显示要等多久。如果要更详细一点,甚至可以显示已经完成的数据(例如 40kb of 64kb)。尽量避免估算完成时间,因为连接速度波动时,没有什么比看到完成时间一直在增长更郁闷的了。

    当用户等待的时间较短时,可以用 loading 指示器。loading 指示器通常是一个旋转变化的小东东(a spinning doo-dad of sorts),但也可以是很简单的文字"loading"。

    你也许会问,为什么极短的时间也要显示这些?loading 指示器是给用户的反馈:用户的操作已经通过,网站正在执行。没有指示器的话,用户不确定是否执行,从而可能企图再次尝试。

延伸阅读

这些建议只是为用户设计时需铭记的事项中的冰山一角。下面是一些可以让你入门(get you started)的关于交互设计和 Web 设计原则的资料:

2009年6月29日星期一

[翻译]减少 reflow

浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构(geometries)的进程叫做 reflow。由于 reflow 是一种浏览器中的用户拦截(user-blocking)操作,所以了解如何减少 reflow 次数,及不同的文档属性(DOM 层级(DOM depth),CSS 效率,不用类型的 style 变化)对 reflow 次数的影响对开发者来说非常必要。有时 reflow 页面中的一个元素会 reflow 它的父元素(译注:这里是复数)以及所有子元素。

有多种用户操作和 DHTML 变化可能会触发 reflow。调整浏览器窗口的大小,用 javascript 计算样式(computed styles),在 DOM 中创建删除元素,改变元素的 class 都会触发 reflow。值得注意的是,有些操作会多次触发 reflow,超出你的想象。下图源自 Steve Souders 的演讲 "Even Faster Web Sites":

从上表可以很明显的看出,在所有浏览器中并非所有 javascript 控制的样式都触发 reflow,即使触发了触发的次数也不尽相同。同时可以看出现代浏览器在控制 reflow 次数方面做的越来越好。

 在 Google,我们通过多种方式对我们的页面及 Web 应用测速,同时 reflow 是我们增加 UI 时考虑的一个关键因素。我们致力于传达轻快的(lively),交互性强的(interactive)和令人愉悦的(delightful)的用户体验。

原则

下面是一些减小 reflow 的原则:

  1. 减少不必要的 DOM 层级(DOM depth)。改变 DOM 树中的一级会导致所有层级的改变,上至根部,下至被改变节点的子节点。这导致大量时间耗费在执行 reflow 上面。
  2. 尽量减少 CSS 规则,去除未用到的 CSS。
  3. 如果做复杂的表现变化,如动画,让它脱离文档流。用绝对定位或 fixed 定位来完成。
  4. 避免不必要的复杂的 CSS 选择器,尤其是后代选择器(descendant selectors),因为为了匹配选择器将耗费更多的 CPU。

在下面的视频中(译注:引用自 youtube,请翻墙),Lindsey 介绍了一些减少 reflow 的方法。

延伸阅读

[翻译]减小 HTML 文件

改善网站速度的一个很明显的方法是减小 HTML 文件的大小。有一些方法,比如:硬压缩(rigid compression),acupuncture-like ID 及改变 class 名(译注:这一句不是很确定,原文:There are several ways to do this, from rigid compression to acupuncture-like ID and class name changes)。下面是一些我们总结的使 HTML 标记更精简的方法。

译注:上面是引用 youtube 的视频,请翻墙观看。

HTML 4

HTML (非XHTML),MIME type 为 text/html ,允许省略一些标签。通过 HTML 4 DTD,你可以省略以下标签(那些所谓可避免的元素,这里用删除线加以标记(感谢一楼的翻译))

</area>
</base>
<body>
</body>
</br>
</col>
</colgroup>
</dd>
</dt>
<head>
</head>
</hr>
<html>
</html>
</img>
</input>
</li>
</link>
</meta>
</option>
</p>
</param>
<tbody>
</tbody>
</td>
</tfoot>
</th>
</thead>
</tr>

比如,你的代码是

<li>List item</li>

可以写为

<li>List item

又比如段落要以

</p>

结尾,你可以只写

<p>My paragraph

甚至可以去掉 html,head,body(把这作为你的编码规范之前请确保这会令你舒服)。

省略标签后 HTML 依然有效,同时减小了文件大小。对一般的页面来说,可以节省 5-20%。

HTML 5

正在发展中的 HTML 5 提供了一些减小文件大小的方法。

比如,页面文档类型声明

<!DOCTYPE html>

对比

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

很显然 HTML 5 的 DTD 更短。

当为页面指定编码时,HTML 5 很易用而且更短:

<meta charset="utf-8">

代替

<meta http-equiv="content-type" content="text/html; charset=utf-8">

通常情况下,浏览器会正确处理 HTML。

另外,在今天的 HTML 5 中,你可以去除声明 MIME 类型的 type 属性,比如

type="text/css"
type="text/javascript"

你可以用

<script>
替代
<script type="text/javascript">
<style>
替代
<style type="text/css">

在所有类型的页面中(甚至是 XHTHML)你可以省略

type="text/css"

HTML 5 使这一切变得更简单。

同时使用上面所有的方法会使文件节省 10%-20%(甚至更多),这取决于你的编码风格和页面中的文本内容数量。代码将更干净,访问者会更快的获取网站内容。在隐私中心项目中我们采用很多这类技术,节省了原始文件大小的 20%。