Flipboard launched during the dawn of the smartphone and tablet as a mobile-first experience, allowing us to rethink content layout principles from the web for a more elegant user experience on a variety of touchscreen form factors.

Now we’re coming full circle and bringing Flipboard to the web. Much of what we do at Flipboard has value independent of what device it’s consumed on: curating the best stories from all the topics, sources, and people that you care about most. Bringing our service to the web was always a logical extension.

As we began to tackle the project, we knew we wanted to adapt our thinking from our mobile experience to try and elevate content layout and interaction on the web. We wanted to match the polish and performance of our native apps, but in a way that felt true to the browser.

Early on, after testing numerous prototypes, we decided our web experience should scroll. Our mobile apps are known for their book-like pagination metaphor, something that feels intuitive on a touch screen, but for a variety of reasons, scrolling feels most natural on the web.

In order to optimize scrolling performance, we knew that we needed to keep paint times below 16ms and limit reflows and repaints. This is especially important during animations. To avoid painting during animations there are two properties you can safely animate: CSS transform and opacity. But that really limits your options.

What if you want to animate the width of an element?

How about a frame-by-frame scrolling animation?

(Notice in the above image that the icons at the top transition from white to black. These are 2 separate elements overlaid on each other whose bounding boxes are clipped depending on the content beneath.)

These types of animations have always suffered from jank on the web, particularly on mobile devices, for one simple reason:

The DOM is too slow.

It’s not just slow, it’s really slow. If you touch the DOM in any way during an animation you’ve already blown through your 16ms frame budget.

Enter <canvas>

Most modern mobile devices have hardware-accelerated canvas, so why couldn’t we take advantage of this? HTML5 games certainly do.  But could we really develop an application user interface in canvas?

Immediate mode vs. retained mode

Canvas is an immediate mode drawing API, meaning that the drawing surface retains no information about the objects drawn into it. This is in opposition to retained mode, which is a declarative API that maintains a hierarchy of objects drawn into it.

The advantage to retained mode APIs is that they are typically easier to construct complex scenes with, e.g. the DOM for your application. It often comes with a performance cost though, as additional memory is required to hold the scene and updating the scene can be slow.

Canvas benefits from the immediate mode approach by allowing drawing commands to be sent directly to the GPU. But using it to build user interfaces requires a higher level abstraction to be productive. For instance something as simple as drawing one element on top of another can be problematic when resources load asynchronously, such as drawing text on top of an image. In HTML this is easily achieved with the ordering of elements or z-index in CSS.

Building a UI in <canvas>

Canvas lacks many of the abilities taken for granted in HTML + CSS.

Text

There is a single API for drawing text: fillText(text, x, y [, maxWidth]). This function accepts three arguments: the text string and x-y coordinates to begin drawing. But canvas can only draw a single line of text at a time. If you want text wrapping, you need to write your own function.

Images

To draw an image into a canvas you call drawImage(). This is a variadic function where the more arguments you specify the more control you have over positioning and clipping. But canvas does not care if the image has loaded or not so make sure this is called only after the image load event.

Overlapping elements

In HTML and CSS it’s easy to specify that one element should be rendered on top of another by using the order of the elements in the DOM or CSS z-index. But remember, canvas is an immediate mode drawing API. When elements overlap and either one of them needs to be redrawn, both have to be redrawn in the same order (or at least the dirtied parts).

Custom fonts

Need to use a custom web font? The canvas text API does not care if a font has loaded or not. You need a way to know when a font has loaded, and redraw any regions that rely on that font. Fortunately, modern browsers have a promise-based API for doing just that. Unfortunately, iOS WebKit (iOS 8 at the time of this writing) does not support it.

Benefits of <canvas>

Given all these drawbacks, one might begin to question selecting the canvas approach over DOM. In the end, our decision was made simple by one simple truth:

You cannot build a 60fps scrolling list view with DOM.

Many (including us) have tried and failed. Scrollable elements are possible in pure HTML and CSS with overflow: scroll (combined with -webkit-overflow-scrolling: touch on iOS) but these do not give you frame-by-frame control over the scrolling animation and mobile browsers have a difficult time with long, complex content.

In order to build an infinitely scrolling list with reasonably complex content, we needed the equivalent of UITableView for the web.

In contrast to the DOM, most devices today have hardware accelerated canvas implementations which send drawing commands directly to the GPU. This means we could render elements incredibly fast; we’re talking sub-millisecond range in many cases.

Canvas is also a very small API when compared to HTML + CSS, reducing the surface area for bugs or inconsistencies between browsers. There’s a reason there is no Can I Use? equivalent for canvas.

A faster DOM abstraction

As mentioned earlier, in order to be somewhat productive, we needed a higher level of abstraction than simply drawing rectangles, text and images in immediate mode. We built a very small abstraction that allows a developer to deal with a tree of nodes, rather than a strict sequence of drawing commands.

RenderLayer

A RenderLayer is the base node by which other nodes build upon. Common properties such as top, left, width, height, backgroundColor and zIndex are expressed at this level. A RenderLayer is nothing more than a plain JavaScript object containing these properties and an array of children.

Image

There are Image layers which have additional properties to specify the image URL and cropping information. You don’t have to worry about listening for the image load event, as the Image layer will do this for you and send a signal to the drawing engine that it needs to update.

Text

Text layers have the ability to render multi-line truncated text, something which is incredibly expensive to do in DOM. Text layers also support custom font faces, and will do the work of updating when the font loads.

Composition

These layers can be composed to build complex interfaces. Here is an example of a RenderLayer tree:

{
frame: [0, 0, 320, 480],
backgroundColor: '#fff',
children: [
{
type: 'image',
frame: [0, 0, 320, 200],
imageUrl: 'http://lorempixel.com/360/420/cats/1/'
},
{
type: 'text',
frame: [10, 210, 300, 260],
text: 'Lorem ipsum...',
fontSize: 18,
lineHeight: 24
}
]
}

Invalidating layers

When a layer needs to be redrawn, for instance after an image loads, it sends a signal to the drawing engine that its frame is dirty. Changes are batched using requestAnimationFrame to avoid layout thrashing and in the next frame the canvas redraws.

Scrolling at 60fps

Perhaps the one aspect of the web we take for granted the most is how a browser scrolls a web page. Browser vendors have gone to great lengths to improve scrolling performance.

It comes with a tradeoff though. In order to scroll at 60fps on mobile, browsers used to halt JavaScript execution during scrolling for fear of DOM modifications causing reflow. Recently, iOS and Android have exposed onscroll events that work more like they do on desktop browsers but your mileage may vary if you are trying to keep DOM elements synchronized with the scroll position.

Luckily, browser vendors are aware of the problem. In particular, the Chrome team has been open about its efforts to improve this situation on mobile.

Turning back to canvas, the short answer is you have to implement scrolling in JavaScript.

The first thing you need is a way to compute scrolling momentum. If you don’t want to do the math the folks at Zynga open sourced a pure logic scroller that fits well with any layout approach.

The technique we use for scrolling uses a single canvas element. At each touch event, the current render tree is updated by translating each node by the current scroll offset. The entire render tree is then redrawn with the new frame coordinates.

This sounds like it would be incredibly slow, but there is an important optimization technique that can be used in canvas where the result of drawing operations can be cached in an off-screen canvas. The off-screen canvas can then be used to redraw that layer at a later time.

This technique can be used not just for image layers, but text and shapes as well. The two most expensive drawing operations are filling text and drawing images. But once these layers are drawn once, it is very fast to redraw them using an off-screen canvas.

In the demonstration below, each page of content is divided into 2 layers: an image layer and a text layer. The text layer contains multiple elements that are grouped together. At each frame in the scrolling animation, the 2 layers are redrawn using cached bitmaps.

Object pooling

During the course of scrolling through an infinite list of items, a significant number of RenderLayers must be set up and torn down. This can create a lot of garbage, which would halt the main thread when collected.

To avoid the amount of garbage created, RenderLayers and associated objects are aggressively pooled. This means only a relatively small number of layer objects are ever created. When a layer is no longer needed, it is released back into the pool where it can later be reused.

Fast snapshotting

The ability to cache composite layers leads to another advantage: the ability to treat portions of rendered structures as a bitmap. Have you ever needed to take a snapshot of only part of a DOM structure? That’s incredibly fast and easy when you render that structure in canvas.

The UI for flipping an item into a magazine leverages this ability to perform a smooth transition from the timeline. The snapshot contains the entire item, minus the top and bottom chrome.

A declarative API

We had the basic building blocks of an application now. However, imperatively constructing a tree of RenderLayers could be tedious. Wouldn’t it be nice to have a declarative API, similar to how the DOM worked?

React

We are big fans of React. Its single directional data flow and declarative API have changed the way people build apps. The most compelling feature of React is the virtual DOM. The fact that it renders to HTML in a browser container is simply an implementation detail. The recent introduction of React Native proves this out.

What if we could bind our canvas layout engine to React components?

Introducing React Canvas

React Canvas adds the ability for React components to render to <canvas> rather than DOM.

The first version of the canvas layout engine looked very much like imperative view code. If you’ve ever done DOM construction in JavaScript you’ve probably run across code like this:

// Create the parent layer
var root = RenderLayer.getPooled();
root.frame = [0, 0, 320, 480]; // Add an image
var image = RenderLayer.getPooled('image');
image.frame = [0, 0, 320, 200];
image.imageUrl = 'http://lorempixel.com/360/420/cats/1/';
root.addChild(image); // Add some text
var label = RenderLayer.getPooled('text');
label.frame = [10, 210, 300, 260];
label.text = 'Lorem ipsum...';
label.fontSize = 18;
label.lineHeight = 24;
root.addChild(label);

Sure, this works but who wants to write code this way? In addition to being error-prone it’s difficult to visualize the rendered structure.

With React Canvas this becomes:

var MyComponent = React.createClass({
render: function () {
return (
<Group style={styles.group}>
<Image style={styles.image} src='http://...' />
<Text style={styles.text}>
Lorem ipsum...
</Text>
</Group>
);
}
}); var styles = {
group: {
left: 0,
top: 0,
width: 320,
height: 480
}, image: {
left: 0,
top: 0,
width: 320,
height: 200
}, text: {
left: 10,
top: 210,
width: 300,
height: 260,
fontSize: 18,
lineHeight: 24
}
};

You may notice that everything appears to be absolutely positioned. That’s correct. Our canvas rendering engine was born out of the need to drive pixel-perfect layouts with multi-line ellipsized text. This cannot be done with conventional CSS, so an approach where everything is absolutely positioned fit well for us. However, this approach is not well-suited for all applications.

css-layout

Facebook recently open sourced its JavaScript implementation of CSS. It supports a subset of CSS like margin, padding, position and most importantly flexbox.

Integrating css-layout into React Canvas was a matter of hours. Check out the example to see how this changes the way components are styled.

Declarative infinite scrolling

How do you create a 60fps infinite, paginated scrolling list in React Canvas?

It turns out this is quite easy because of React’s diffing of the virtual DOM. In render() only the currently visible elements are returned and React takes care of updating the virtual DOM tree as needed during scrolling.

var ListView = React.createClass({
getInitialState: function () {
return {
scrollTop: 0
};
}, render: function () {
var items = this.getVisibleItemIndexes().map(this.renderItem);
return (
<Group
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
onTouchCancel={this.handleTouchEnd}>
{items}
</Group>
);
}, renderItem: function (itemIndex) {
// Wrap each item in a <Group> which is translated up/down based on
// the current scroll offset.
var translateY = (itemIndex * itemHeight) - this.state.scrollTop;
var style = { translateY: translateY };
return (
<Group style={style} key={itemIndex}>
<Item />
</Group>
);
}, getVisibleItemIndexes: function () {
// Compute the visible item indexes based on `this.state.scrollTop`.
}
});

To hook up the scrolling, we use the Scroller library to setState() on our ListView component.

...

// Create the Scroller instance on mount.
componentDidMount: function () {
this.scroller = new Scroller(this.handleScroll);
}, // This is called by the Scroller at each scroll event.
handleScroll: function (left, top) {
this.setState({ scrollTop: top });
}, handleTouchStart: function (e) {
this.scroller.doTouchStart(e.touches, e.timeStamp);
}, handleTouchMove: function (e) {
e.preventDefault();
this.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
}, handleTouchEnd: function (e) {
this.scroller.doTouchEnd(e.timeStamp);
} ...

Though this is a simplified version it showcases some of React’s best qualities. Touch events are declaratively bound in render(). Each touchmove event is forwarded to the Scroller which computes the current scroll top offset. Each scroll event emitted from the Scroller updates the state of the ListView component, which renders only the currently visible items on screen. All of this happens in under 16ms because React’s diffing algorithm is very fast.

See the ListView source code for the complete implementation.

Practical applications

React Canvas is not meant to completely replace the DOM. We utilize it in performance-critical rendering paths in our mobile web app, primarily the scrolling timeline view.

Where rendering performance is not a concern, DOM may be a better approach. In fact, it’s the only approach for certain elements such as input fields and audio/video.

In a sense, Flipboard for mobile web is a hybrid application. Rather than blending native and web technologies, it’s all web content. It mixes DOM-based UI with canvas rendering where appropriate.

A word on accessibility

This area needs further exploration. Using fallback content (the canvas DOM sub-tree) should allow screen readers such as VoiceOver to interact with the content. We’ve seen mixed results with the devices we’ve tested. Additionally there is a standard for focus management that is not supported by browsers yet.

One approach that was raised by Bespin in 2009 is to keep a parallel DOM in sync with the elements rendered in canvas. We are continuing to investigate the right approach to accessibility.

Conclusion

In the pursuit of 60fps we sometimes resort to extreme measures. Flipboard for mobile web is a case study in pushing the browser to its limits. While this approach may not be suitable for all applications, for us it’s enabled a level of interaction and performance that rivals native apps. We hope that by releasing the work we’ve done with React Canvas that other compelling use cases might emerge.

Head on over to flipboard.com on your phone to see what we’ve built, or if you don’t have a Flipboard account, check out a couple of magazines to get a taste of Flipboard on the web. Let us know what you think.

Special thanks to Charles, Eugene and Anh for edits and suggestions.

[转]60fps on the mobile web的更多相关文章

  1. front end about mobile web techs

    WEB OF DEVICES http://www.w3.org/standards/webofdevices/ MOBILE WEB http://www.w3.org/standards/webd ...

  2. Mobile Web中URL设计问题

    自己虽然也注册了CSDN账号,但是没有在上面发表过博客等内容.不过经常在Google里面搜索相关内容时,会显示csdn的结果.这也说明国内很多IT人员都会在CSDN发表博客,记录解决问题过程或者想法. ...

  3. swipe.js 2.0 轻量级框架实现mobile web 左右滑动

    属性总结笔记如下: <style> .swipe { overflow: hidden; //隐藏溢出 清楚浮动 visibility: hidden; //规定元素不可见 (可以设置,当 ...

  4. Mobile Web调试工具Weinre (reproduce)

    Mobile Web调试工具Weinre 现在.将来,用移动设备上网越来越成为主流.但对于开发者们来说,移动web的调试一直是个难题,前期可以使用模拟器来协助调试,但到了真机调试阶段就让人非常头痛.而 ...

  5. 打造离线使用的Mobile Web App

    最近公司举办技术大赛,我和同事一起制作了一个叫做10K Hours的Mobile Web App,可以帮助你通过一万小时的努力,成为某个领域的专家.正好前段时间翻译了一本书<HTML5 Mobi ...

  6. Introducing the Accelerated Mobile Pages Project, for a faster, open mobile web

    https://googleblog.blogspot.com/2015/10/introducing-accelerated-mobile-pages.html October 7, 2015 Sm ...

  7. 开发库比较(3) - Mobile Web 开发 - Sencha, jquerymobiel, phonejs, jqtouch, jqmobi

    我们一直坚信Html/css在界面上最终会一统江湖,因为在众多的界面编写中,qt,gtk,wpf,win form, wxwidgets等等,只有Html/CSS是真正拥有统一标准,只有这个有潜力作用 ...

  8. Lesson 3: The Amazing New Mobile Web

    Lesson 3: The Amazing New Mobile Web Article 1: This is Responsive by Brad Frost 各种响应式网站设计的资源. Artic ...

  9. Mobile Web开发 处理设备的横竖屏

    为了应对移动设备屏幕的碎片化,我们在开发Mobile Web应用时,一个最佳实践就是采用流式布局,保证最大可能地利用有限的屏幕空间.由于屏幕存在着方向性,用户在切换了屏幕的方向后,有些设计上或实现上的 ...

随机推荐

  1. javaWeb web.xml 配置

    <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http:// ...

  2. inputstream输出为String

    import java.io.IOException; import java.io.InputStream; import org.apache.http.HttpEntity; import or ...

  3. 每一个JavaScript开发者都应该知道的10道面试题

    JavaScript十分特别.而且差点儿在每一个大型应用中起着至关关键的数据.那么,究竟是什么使JavaScript显得与众不同,意义非凡? 这里有一些问题将帮助你了解其真正的奥妙所在:   1.你能 ...

  4. Mysql 数据迁移后 启动出错

    今天上班后不知道为什么,mysql一直无法启动,折腾了半天于是决定重装 我本地的server用的是wamp , 重装的时候, 要进行数据备份 , 我使用的最简单粗暴的备份方式, 就是直接进入到mysq ...

  5. yii自己定义CLinkPager分页

    在components中自己定义LinkPager.并继承CLinkPager 代码例如以下: <? php /** * CLinkPager class file. * * @author l ...

  6. [JZOJ 5852] [NOIP2018提高组模拟9.6] 相交 解题报告 (倍增+LCA)

    题目链接: http://172.16.0.132/senior/#main/show/5852 题目: 题目大意: 多组询问,每次询问树上两条链是否相交 题解: 两条链相交并且仅当某一条链的两个端点 ...

  7. sql/plus无法显示数据库问题

    登录PL/SQL Developer 这里省略Oracle数据库和PL/SQL Developer的安装步骤,注意在安装PL/SQL Developer软件时,不要安装在Program Files ( ...

  8. Spark 运行机制及原理分析

  9. Sublimi Text3 下Emmet使用技巧

    Emmet真的好用,可以少写很多代码. 初始化文档 HTML文档需要包含一些固定的标签,比如<html>.<head>.<body>等,现在你只需要1秒钟就可以输入 ...

  10. Python3基础笔记---模块

    参考博客:Py西游攻关之模块 模块的概念: 我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式.在Python中,一个.py文件就称之为 ...