HTML5 Rocks
Understand how paints affect your performance.
Understand how different CSS property combinations can influence the paint-time of your page.
Discover how the Google I/O 2013 experiment was developed.
Using clipping and masking from CSS.
This article describes a way to do as little work as possible, yet still serve beautiful images for both high and low DPI screens.
TL;DR
Custom Filters, or CSS Shaders as they used to be called, allow you to use the power of WebGL's shaders with your DOM content. Since in the current implementation the shaders used are virtually the same as those in WebGL, you need to take a step back and understand some 3D terminology and a little bit of the graphics pipeline.
I've included a recorded version of a presentation I recently delivered to LondonJS. In the video I step through an overview of the 3D terminology you need to understand, what the different variable types are that you'll encounter, and how you can start playing with Custom Filters today. You should also grab the slides so you can play with the demos yourself.
Introduction to Shaders
I've previously written an introduction to shaders which will give you a good breakdown of what shaders are and how you can use them from a WebGL point of view. If you've never dealt with shaders it's something of a required read before you go much further, because many of the Custom Filters concepts and language hinges on the existing WebGL shader terminology.
So with that said, let's enable Custom Filters and plough on!
Enabling Custom Filters
Custom Filters are available in both Chrome and Canary as well as Chrome for Android. Simply head over to about:flags and search for "CSS Shaders", enable them and restart the browser. Now you're good to go!
The syntax
Custom Filters expands on the set of filters that you can already apply, like blur or sepia, to your DOM elements. Eric Bidelman wrote a great playground tool for those, which you should check out.
To apply a Custom Filter to a DOM element you use the following syntax:
.customShader {
-webkit-filter:
custom(
url(vertexshader.vert)
mix(url(fragment.frag) normal source-atop),
/* Row, columns - the vertices are made automatically */
4 5,
/* We set uniforms; we can't set attributes */
time 0)
}
You'll see from this that we declare our vertex and fragment shaders, the number of rows and columns we want our DOM element to get broken down into, and then any uniforms we want to pass through.
A final thing to point out here is that we use the mix() function around the fragment shader with a blend mode (normal), and a composite mode (source-atop). Let's take a look at the fragment shader itself to see why we even need a mix() function.
Pixel Pushing
If you're familiar with WebGL's shaders you'll notice that in Custom Filters things are a little different. For one we don't create the texture(s) that our fragment shader uses to fill in the pixels. Rather the DOM content that has the filter applied gets mapped to a texture automatically, and that means two things:
- For security reasons we can't query individual pixel color values of the DOM's texture
- We don't (at least in current implementations) set the final pixel color ourselves, i.e.
gl_FragColor is off limits. Rather, it's assumed that you will want to render the DOM content, and what you get to do is manipulate its pixels indirectly through css_ColorMatrix and css_MixColor.
That means our Hello World of fragment shaders looks more like this:
void main() {
css_ColorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
css_MixColor = vec4(0.0, 0.0, 0.0, 0.0);
// umm, where did gl_FragColor go?
}
Each pixel of the DOM content is multiplied by the css_ColorMatrix, which in the above case does nothing as its the identity matrix and changes none of the RGBA values. If we did want to, say, just keep the red values we would use a css_ColorMatrix like this:
// keep only red and alpha
css_ColorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0);
You can hopefully see that as you multiply the 4D (RGBA) pixel values by the matrix that you get a manipulated pixel value out of the other side, and in this case one that zeros out the green and blue components.
The css_MixColor is mainly used as a base color that you want to, well, mix in with your DOM content. The mixing is done through the blend modes that you'll be familiar with from art packages: overlay, screen, color dodge, hard light and so on.
There are plenty of ways that these two variables can manipulate the pixels, and included in my presentation is a demo that you can play around with. You should check out the Custom Filters specification to get a better handle on how the blend and composite modes interact.
Vertex Creation
In WebGL we take full responsibility for the creation of our mesh's 3D points, but in Custom Filters all you have to do is specify the number of rows and columns that you want and the browser will automatically break down your DOM content into a bunch of triangles:
An image being broken down into rows and columns
Each one of those vertices then gets passed through to our vertex shader for manipulation, and that means we can start moving them around in 3D space as we need. It's not long before you can make some pretty crazy effects!
An image being warped by an accordion effect
Animating with Shaders
Bringing in animations to your shaders is what makes them fun and engaging. To do that you simply use a transition (or animation) in your CSS to update uniform values:
.shader {
/* transition on the filter property */
-webkit-transition: -webkit-filter 2500ms ease-out;
-webkit-filter: custom(
url(vshader.vert)
mix(url(fshader.frag) normal source-atop),
1 1,
time 0);
}
.shader:hover {
-webkit-filter: custom(
url(vshader.vert)
mix(url(fshader.frag) normal source-atop),
1 1,
time 1);
}
So the thing to notice in the code above is that time is going to ease from 0 to 1 during the transition. Inside the shader we can declare the uniform time and use whatever its current value is:
uniform float time;
uniform mat4 u_projectionMatrix;
attribute vec4 a_position;
void main() {
// copy a_position to position - attributes are read only!
vec4 position = a_position;
// use our time uniform from the CSS declaration
position.x += time;
gl_Position = u_projectionMatrix * position;
}
Get Playing!
Custom Filters are great fun to play with, and the amazing effects you can create are difficult (and in some cases impossible) without them. It is still early days, and things are changing quite a bit, but adding them will add a little bit of showbiz to your projects, so why not give them a go?
Additional Resources
:scope is defined in CSS Selectors 4 as:
A pseudo-class which represents any element that is in the contextual reference element set. This is is a (potentially empty) explicitly-specified set of elements, such as that specified by the querySelector(), or the parent element of a <style scoped> element, which is used to "scope" a selector so that it only matches within a subtree.
An example of using this guy is within a <style scoped> (more info):
<style>
li {
color: blue;
}
</style>
<ul>
<style scoped>
li {
color: red;
}
:scope {
border: 1px solid red;
}
</style>
<li>abc</li>
<li>def</li>
<li>efg</li>
</ul>
<ul>
<li>hij</li>
<li>klm</li>
<li>nop</li>
</ul>
Note: <style scoped> can be enabled in Chrome using the "Enable experimental WebKit features" flag in about:flags.
This colors the li elements in the first ul red and, because of the :scope rule, puts a border around the ul. That's because in the context of this <style scoped>, the ul matches :scope. It's the local context. If we were to add a :scope rule in the outer <style> it would match the entire document. Essentially, equivalent to :root.
Contextual elements
You're probably aware of the Element version of querySelector() and querySelectorAll(). Instead of querying the entire document, you can restrict the result set to a contextual element:
<ul>
<li id="scope"><a>abc</a></li>
<li>def</li>
<li><a>efg</a></li>
</ul>
<script>
document.querySelectorAll('ul a').length; // 2
var scope = document.querySelector('#scope');
scope.querySelectorAll('a').length; // 1
</script>
When these are called, the browser returns a NodeList that's filtered to only include the set of nodes that a.) match the selector and b.) which are also descendants of the context element. So in the the second example, the browser finds all a elements, then filters out the ones not in the scope element. This works, but it can lead to some bizarre behavior if you're not careful. Read on.
When querySelector goes wrong
There's a really important point in the Selectors spec that people often overlook. Even when querySelector[All]() is invoked on an element, selectors still evaluate in the context of the entire document. This means unanticipated things can happen:
scope.querySelectorAll('ul a').length); // 1
scope.querySelectorAll('body ul a').length); // 1
WTF! In the first example, ul is my element, yet I'm still able to use it and matches nodes. In the second, body isn't even a descendant of my element, but "body ul a" still matches. Both of these are confusing and not what you'd expect.
It's worth making the comparison to jQuery here, which takes the right approach and does what you'd expect:
$(scope).find('ul a').length // 0
$(scope).find('body ul a').length // 0
...enter :scope to solve these semantic shenanigans.
Fixing querySelector with :scope
WebKit recently landed support for using the :scope pseudo-class in querySelector[All](). You can test it in Chrome Canary 27.
You can use it restrict selectors to a context element. Let's see an example. In the following, :scope is used to "scope" the selector to the scope element's subtree. That's right, I said scope three times!
scope.querySelectorAll(':scope ul a').length); // 0
scope.querySelectorAll(':scope body ul a').length); // 0
scope.querySelectorAll(':scope a').length); // 1
Using :scope makes the semantics of the querySelector() methods a little more predictable and inline with what others like jQuery are already doing.
Performance win?
Not yet :(
I was curious if using :scope in qS/qSA gives a performance boost. So...like a good engineer I threw together a test. My rationale: less surface area for the browser to do selector matching means speedier lookups.
In my experiment, WebKit currently takes ~1.5-2x longer than not using :scope. Drats! When crbug.com/222028 gets fixed, using it should theoretically give you a slight performance boost over not using it.
Shadow DOM is a difficult topic to wrap your head around. It's just complex. It introduces unfamiliar concepts that we're not used to on the web. Shadow boundaries, styling scoping, event retargeting, insertion points, shadow insertion points, host nodes, distributed nodes,...the lingo goes on and on.
TRY THE DEMO
One thing that's conceptually taxing about Shadow DOM is the way your final product (DOM) is rendered by the browser. Nodes from the host node are magically swizzled into a ShadowRoot's insertion points, yet logically, still remain in the host node. Weird! So at render time, they appear as part of the shadow tree and not the original host. How this rendering takes place is one of the most confusing pieces of Shadow DOM.
A few days ago, I released a tool I've been working on called Shadow DOM Visualizer to help lessen the learning curve.
It allows you to visually see how Shadow DOM renders in the browser, something DevTools lacks today. Both black code blocks on the left are editable. Try changing the <content> insertion points, removing, or adding new ones to see how the composited (rendered) tree is affected on the right.
Mouse over the nodes in the graph to highlight the relevant markup on the left. Yay for d3.js! Blue nodes are coming from the host node. Yellow nodes come from the Shadow DOM. <content> insertion points are the bridge
between the two worlds. Because they're logically in the Shadow DOM, they're colored yellow. Their blue border indicates that they invite blue host nodes into the rendering party.
Shadow DOM is available in Chrome 25 and the <template> element is available in Chrome 26 (although you only need the first to try the demo).
Continuous painting mode for paint profiling is now available in
Chrome
Canary. This
article explains how you identify a problem in page painting time and how you
can use this new tool to detect bottlenecks in painting performance.
Investigating painting time on your page
So you noticed that your page doesn't scroll smoothly. This is how you would
start tackling the problem. For our example, we'll use the demo page Things We Left On The Moon by Dan Cederholm as our example.
You open the Web Inspector, start a Timeline recording and scroll your page up
and down. Then you look at the vertical timelines, that show you what happened
in each frame.
If you see that most time is spent painting (big green bars above 60fps), you
need to take a closer look at why this is happening. To investigate your paints,
use the Show paint rectangles setting of the Web Inspector (cog icon in the
bottom right corner of the Web Inspector). This will show you the regions where
Chrome paints.
There are different reasons for Chrome to repaint areas of the page:
-
DOM nodes get changed in JavaScript, which causes Chrome to recalculate the
layout of the page.
-
Animations are playing that get updated in a frame-based cycle.
-
User interaction, like hovering, causes style changes on certain elements.
-
Any other operation that causes the page layout to change.
As a developer you need to be aware of the repaints happening on your page.
Looking at the paint rectangles is a great way of doing that. In the example
screenshot above you can see that the whole screen is covered in a big paint
rectangle. This means the whole screen is repainted as you scroll, which is not
good. In this specific case this is caused by the CSS style
background-attachment:fixed which causes the background image of the page to
stay at the same position while the content of the page moves on top of it as
you scroll.
If you identify that the repaints cover a big area and/or take a long time, you
have two options:
You can try to change the page layout to reduce the amount of painting. If
possible Chrome paints the visible page only once and adds parts that have
not been visible as you scroll down. However, there are cases when Chrome
needs to repaint certain areas. For example the CSS rule position:fixed,
which is often used for navigation elements that stay in the same position,
can cause these repaints.
If you want to keep your page layout, you can try to reduce the painting cost
of the areas that get repainted. Not every CSS style has the same painting
cost, some have little impact, others a lot. Figuring out the painting costs
of certain styles can be a lot of work. You can do this by toggling styles in
the Elements panel and looking at the difference in the Timeline recording,
which means switching between panels and doing lots of recordings. This is
where continuous painting mode comes into play.
Continuous painting mode
Continuous painting mode is a tool that helps you identify which elements
are costly on the page. It puts the page into an always repainting state,
showing a counter of how much painting work is happening. Then, you can hide
elements and mutate styles, watching the counter, in order to figure out what is
slow.
Setup
In order to use continuous painting mode you need to use Chrome
Canary.
On Linux systems (and some Macs) you need to make sure that Chrome runs in
compositing mode. This can be permanently enabled using the GPU compositing on
all pages setting in about:flags.
How To Begin
Continuous painting mode can be enabled via the checkbox Enable continuous
page repainting in the Web Inspector's settings (cog icon in the bottom right
corner of the Web Inspector).
The small display in the top right corner shows you the measured paint times in
milliseconds. More specifically it shows:
-
The last measured paint time on the left.
-
The minimum and maximum of the current graph on the right.
-
A bar chart displaying the history of the last 80 frames on the bottom (the
line in the chart indicates 16ms as a reference point).
The paint time measurements are dependent on screen resolution, window size and
the hardware Chrome is running on. Be aware that these things are likely to be
different for your users.
Workflow
This is how you can use continuous painting mode to track down elements and
styles that add a lot of painting cost:
-
Open the Web Inspector's settings and check Enable continuous page
repainting.
-
Go to the Elements panel and traverse the DOM tree with the arrow keys or by
picking elements on the page.
-
Use the H keyboard shortcut, a newly introduced helper, to toggle visibility on an
element.
-
Look at the paint time graph and try to spot an element that adds a lot of
painting time.
-
Go through the CSS styles of that element, toggling them on and off while
looking at the graph, to find the style that causes the slow down.
-
Change this style and do another Timeline recording to check if this made
your page perform better.
The animation below shows toggling styles and its affect on paint time:
This example demonstrates how turning either one of the CSS styles box-shadow
or border-radius off, reduces the painting time by a big amount. Using both box-shadow andborder-radius on an element leads to very expensive painting
operations, because Chrome can't optimize for this. So if you have an element
that gets a lot of repaints, like in the example, you should avoid this
combination.
Notes
Continuous painting mode repaints the whole visible page. This is usually
not the case when browsing a web page. Scrolling usually only paints the parts
that haven't been visible before. And for other changes on the page, only the
smallest possible area is repainted. So check with another Timeline recording if
your style improvements actually had an impact on the paint times of your page.
When using continuous painting mode you might discover that e.g. the CSS
styles border-radius and box-shadow add a lot of painting time. It is not
discouraged to use those features in general, they are awesome and we are happy
they are finally here. But it's important to know when and where to use them.
Avoid using them in areas with lots of repaints and avoid overusing them in
general.
Learn more about painting and related topics on jankfree.com
Eberhard Gräther is student of MultiMediaTechnology at Salzburg University of Applied Sciences. He interned in the Chrome GPU team from 10/2012 to 03/2013 where he worked on rendering profiling tools. Follow him at @egraether or visit his site to find graphics demos and web games he has built.
Live Demo
Click below for a demo where Paul Irish uses continuous painting to identify a uniquely expensive paint operation.
More good news from our old friend WebRTC...
To be precise: three pieces of good news and a couple of minor API changes.
RTCDataChannel for Chrome
RTCDataChannel has been implemented in Chrome, and there's a great little demo at simpl.info/dc.
This demo shows peer-to-peer communication of arbitrary data – in less than a hundred lines of code. You'll need Chrome 25 or above for this, which at this point means Beta or Canary.
RTCDataChannel makes the most of features built into RTCPeerConnection – not least, the use if the ICE framework to get through firewalls and NATs – and has lots of potential applications where low latency is paramount: for gaming, remote desktop applications, real-time text chat and file transfer.
For more information about RTCDataChannel, take a look at Getting Started with WebRTC.
API changes
Less exciting, but still important: from Chrome 26, some RTCPeerConnection and MediaStream API properties have become getter methods:
- MediaStream now has the
getAudioTracks() method instead of the audioTracks property, and getVideoTracks() instead of videoTracks.
- RTCPeerConnection now has
getLocalStreams() instead of localStreams, and getRemoteStreams() instead of remoteStreams.
To get a glimpse of MediaStream in action, take a look at the simpl.info/gum getUserMedia demo. The stream variable is in global scope: examine it from the console. Likewise for RTCPeerConnection at simpl.info/pc: the RTCPeerConnection objects pc1 and pc2 are in global scope.
Chrome <=> Firefox
And in case you missed it, Chrome can now 'talk' to Firefox.
You can try this out now at webrtc.org/demo, which has full instructions, links to source code, and information about API differences.
Tip of the hat to those at Mozilla and Google who made it all happen.
Happy coding! And please let us know of any bugs, either by commenting on this post, or at crbug.com/new.
...and don't forget, you can always get up-to-date implementation information from the excellent chromestatus.com.
|