MockingBird

记录 Web 前端成长旅程


  • 首页

  • 分类

  • 归档

  • 标签

使用 will-change 来提升浏览器渲染效果

发表于 2015-07-08   |   分类于 css3 , turorials   |  

今天偶然在youtube上看到一段视频,讲诉的是作者在他的网页中用到了background-attachment: fixed;来实现背景图固定效果,但是这种是实现方式导致页面在滚动时,会出现页面卡顿的现象,这很明显降低了用户的体验。 下面介绍通过will-change来部分规避这个问题。

原始版本

让我们先来重现一下产生问题的场景,我写了一个简单的DEMO:
http://codepen.io/CodingMonkeyzh/pen/zGWyYM

可以看到, 我们在.first块中放了一张背景图片,并且通过background-attachment:fixed来实现背景图固定的效果,你可以滚动页面感受一下效果,是不是很酷炫?下面是核心的样式:

1
2
3
4
5
.first {
background: url('http://placehold.it/800x500') no-repeat center center;
background-attachment: fixed;
background-size: cover;
}

通过这种方式实现的效果表现力如何呢? 打开Chrome DevTool 的 Timeline来调试一下:
background-attachment: fixed;的表现力
这看上去并不怎么样, 很多时候都掉到了30fps以下。

改进版本

下面,我们试试用will-change来做一下浏览器优化。同样,我基于前面的例子,放弃了之前的实现方式,加入了will-change来优化浏览器渲染效果,这是我的DEMO:
http://codepen.io/CodingMonkeyzh/pen/YXadXP

我们没有在使用background-attachment: fixed;来实现这个效果,取而代之的是,利用:before,在其中放入一张position:fixed;的背景图片,然后用will-change: transform做浏览器渲染优化。下面是核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
.first::before {
content: ' ';
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: url('http://placehold.it/800x500') no-repeat center center;
background-size: cover;
will-change: transform;
z-index: -1;
}

那通过这种方式进行优化,表现力究竟如何呢?我们采用同样的方式对页面进行调试,结果如下:
will-chagne 优化后的效果
可以看到,页面的帧率明显得到了提升!页面滚动时变得流畅了许多。

那么,will-change到底是什么?

will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。

will-change 的兼容性

然后,will-change 目前还在实验结果,浏览器的支持度不尽人意,IE目前没有支持该属性,Chrome 36+, Firefox 38+ 也才开始支持这一属性。

注意事项

正如MDN 上面提到的那样,will-change虽然可以提升渲染效果,但是我们需要引起注意:

  • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。
  • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。
  • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。
  • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

推荐阅读

  • css3 实现 drop-box
  • css3 box shadow
  • CSS3 Media Query

制作图片倾斜(tilt)效果

发表于 2015-06-26   |   分类于 css3 , turorials   |  

原文来自:

http://tympanus.net/codrops/2015/05/28/image-tilt-effect/

所谓的倾斜效果,我也不知如何用语言描述,那就直接看Demo啦,下面我们会对这个效果的实现原理逐步分析:
http://codepen.io/CodingMonkeyzh/pen/jPYNyr

文档结构

对一个图片添加该效果,首先,我们需要一个具有宽高的容器。DOM 结构非常简单。

1
2
3
<div class="container">
<img class="tilt-effect" src="http://placehold.it/500x300" alt="" />
</div>

上面这段结构经过脚本处理之后,会被替换成下面的结构:

1
2
3
4
5
6
7
<div class="container">
<div class="tilt">
<div class="tilt__back" style="background-image: url(http://placehold.it/500x300);"></div>
<div class="tilt__front" style="opacity: 0.7; -webkit-transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); background-image: url(http://placehold.it/500x300);"></div>
<div class="tilt__front" style="opacity: 0.7; -webkit-transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); transform: perspective(1000px) translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg); background-image: url(http://placehold.it/500x300);"></div>
</div>
</div>

脚本分析

我们利用了filtfx.js这个插件对上面的图片进行处理, 来实现倾斜效果。我在原来的代码中加入了一些注释,来帮助我们理解。下面我们对该插件的核心代码进行分析。

1
2
3
4
5
6
7
function TiltFx(el, options) {
this.el = el;
this.options = extend({}, this.options);
extend(this.options, options);
this._init();
this._initEvents();
}

这是构造函数,如果我们的文档中,加入了上面的插件,那么插件会遍历文档中具有tilt-effet的img元素,来调用构造函数TiltFx():

1
2
3
4
5
6
function init() {
// 遍历所有拥有‘title-effect’类的img元素
[].slice.call(document.querySelectorAll('img.tilt-effect')).forEach(function(img) {
new TiltFx(img, JSON.parse(img.getAttribute('data-tilt-options')));
});
}

TiltFx()具有一个原型属性,两个原型方法。原型属性配置了一些默认的参数用于调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 默认参数
*/
TiltFx.prototype.options = {
extraImgs: 2, // 额外的辅助图片数量
opacity: 0.7,
bgfixed: true, // 底图是否固定
movement: { // 这是一些用于移动的参数
perspective: 1000,
translateX: -10,
translateY: -10,
translateZ: 20,
rotateX: 2,
rotateY: 2,
rotateZ: 0
}
}

第一个原型方法是_init(),用于初始化DOM结点,生成我们的目标DOM结点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
TiltFx.prototype._init = function() {
this.tiltWrapper = document.createElement('div');
this.tiltWrapper.className = 'tilt';

// main image element.
this.tiltImgBack = document.createElement('div');
this.tiltImgBack.className = 'tilt__back';
this.tiltImgBack.style.backgroundImage = 'url(' + this.el.src + ')';
this.tiltWrapper.appendChild(this.tiltImgBack);

// image elements limit.
if (this.options.extraImgs < 1) {
this.options.extraImgs = 1;
} else if (this.options.extraImgs > 5) {
this.options.extraImgs = 5;
}

if (!this.options.movement.perspective) {
this.options.movement.perspective = 0;
}

// add the extra image elements.
this.imgElems = [];
for (var i = 0; i < this.options.extraImgs; ++i) {
var el = document.createElement('div');
el.className = 'tilt__front';
el.style.backgroundImage = 'url(' + this.el.src + ')';
el.style.opacity = this.options.opacity;
this.tiltWrapper.appendChild(el);
this.imgElems.push(el);
}

if (!this.options.bgfixed) {
this.imgElems.push(this.tiltImgBack);
++this.options.extraImgs;
}

// add it to the DOM and remove original img element.
this.el.parentNode.insertBefore(this.tiltWrapper, this.el);
this.el.parentNode.removeChild(this.el);

// tiltWrapper properties: width/height/left/top
this.view = {
width: this.tiltWrapper.offsetWidth,
height: this.tiltWrapper.offsetHeight
};
};

另外一个原型方式是用于监听鼠标事件之类的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
TiltFx.prototype._initEvents = function() {
var self = this,
moveOpts = self.options.movement;

// mousemove event..
this.tiltWrapper.addEventListener('mousemove', function(ev) {
requestAnimationFrame(function() {
// mouse position relative to the document.
var mousepos = getMousePos(ev),
// document scrolls.
docScrolls = {
left: document.body.scrollLeft + document.documentElement.scrollLeft,
top: document.body.scrollTop + document.documentElement.scrollTop
},
bounds = self.tiltWrapper.getBoundingClientRect(),
// mouse position relative to the main element (tiltWrapper).
relmousepos = {
x: mousepos.x - bounds.left - docScrolls.left,
y: mousepos.y - bounds.top - docScrolls.top
};

// configure the movement for each image element.
for (var i = 0, len = self.imgElems.length; i < len; ++i) {
var el = self.imgElems[i],
rotX = moveOpts.rotateX ? 2 * ((i + 1) * moveOpts.rotateX / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.rotateX / self.options.extraImgs) : 0,
rotY = moveOpts.rotateY ? 2 * ((i + 1) * moveOpts.rotateY / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateY / self.options.extraImgs) : 0,
rotZ = moveOpts.rotateZ ? 2 * ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) : 0,
transX = moveOpts.translateX ? 2 * ((i + 1) * moveOpts.translateX / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.translateX / self.options.extraImgs) : 0,
transY = moveOpts.translateY ? 2 * ((i + 1) * moveOpts.translateY / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateY / self.options.extraImgs) : 0,
transZ = moveOpts.translateZ ? 2 * ((i + 1) * moveOpts.translateZ / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateZ / self.options.extraImgs) : 0;

el.style.WebkitTransform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';
el.style.transform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';
}
});
});

// reset all when mouse leaves the main wrapper.
this.tiltWrapper.addEventListener('mouseleave', function(ev) {
setTimeout(function() {
for (var i = 0, len = self.imgElems.length; i < len; ++i) {
var el = self.imgElems[i];
el.style.WebkitTransform = 'perspective(' + moveOpts.perspective + 'px) translate3d(0,0,0) rotate3d(1,1,1,0deg)';
el.style.transform = 'perspective(' + moveOpts.perspective + 'px) translate3d(0,0,0) rotate3d(1,1,1,0deg)';
}
}, 60);

});

// window resize
window.addEventListener('resize', throttle(function(ev) {
// recalculate tiltWrapper properties: width/height/left/top
self.view = {
width: self.tiltWrapper.offsetWidth,
height: self.tiltWrapper.offsetHeight
};
}, 50));
};

我们可以看到,监听mousemove的事件处理函数中的计算比较复杂,关键的部分就是在这里:

1
2
3
4
5
6
7
8
9
10
var el = self.imgElems[i],
rotX = moveOpts.rotateX ? 2 * ((i + 1) * moveOpts.rotateX / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.rotateX / self.options.extraImgs) : 0,
rotY = moveOpts.rotateY ? 2 * ((i + 1) * moveOpts.rotateY / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateY / self.options.extraImgs) : 0,
rotZ = moveOpts.rotateZ ? 2 * ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.rotateZ / self.options.extraImgs) : 0,
transX = moveOpts.translateX ? 2 * ((i + 1) * moveOpts.translateX / self.options.extraImgs) / self.view.width * relmousepos.x - ((i + 1) * moveOpts.translateX / self.options.extraImgs) : 0,
transY = moveOpts.translateY ? 2 * ((i + 1) * moveOpts.translateY / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateY / self.options.extraImgs) : 0,
transZ = moveOpts.translateZ ? 2 * ((i + 1) * moveOpts.translateZ / self.options.extraImgs) / self.view.height * relmousepos.y - ((i + 1) * moveOpts.translateZ / self.options.extraImgs) : 0;

el.style.WebkitTransform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';
el.style.transform = 'perspective(' + moveOpts.perspective + 'px) translate3d(' + transX + 'px,' + transY + 'px,' + transZ + 'px) rotate3d(1,0,0,' + rotX + 'deg) rotate3d(0,1,0,' + rotY + 'deg) rotate3d(0,0,1,' + rotZ + 'deg)';

这里我们根据鼠标的位置,计算出了各个图层对应的偏移量和旋转角度,然后对它们进行变换即可。
最后mouseleave之后,我们再把个个图层恢复到初始位置就行了。

Demo:http://codepen.io/CodingMonkeyzh/pen/jPYNyr

推荐阅读

  • 阻止HTML中的图片拖动

函数的提升

发表于 2015-06-04   |   分类于 javascript   |  

我们知道,对于所有的变量,无论在函数体的什么地方声明,都会在后台被提升到函数体顶部。

这个规则对于函数同样适用,其原因在于函数只是分配给变量的对象。唯一需要引起注意的地方在于当适用函数声明时,函数定义也会被提升,而不仅仅是函数声明被提升。看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test() {
console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"

function foo() {

}

var bar = function () {

};
}

test();

test()函数中的foo和bar的声明被提升到了顶部;同时,需要注意的是:foo和bar的区别在于前者的定义得到了提升,而后者的定义没有提升,这也就是console.log(typeof bar)输出undefined的原因。

所以,需要记住的是:函数表达式仅仅是变量被提升,而函数的实现没有被提升。
上面的代码中,后台解析的代码类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test() {
var bar;
function foo() {

}
console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"

var bar = function () {

};
}

test();

javascript 代码规范

发表于 2015-06-01   |   分类于 javascript , 规范   |  

以下编码风格来自于《编写可维护的 javascript》一书中,加上个人总结,自代表个人风格,仅供参考。

##缩进
缩进可以采用2格缩进或者4格缩进,具体使用哪个看团队的代码规范和自己的爱好。

##行尾分号
行尾一律加上;分号,除非该行需要折行。

##行的长度
单行不应该超过80个字符,如果超过,应该折行编写。

##折行
当一行长度达到单行最大字符限制时,手动将一行拆成两行。遵守:

  1. 在运算符后换行
  2. 下一行增加两层缩进
1
2
callAFunction(argument1, argument2, argument3, argument4, argument5, argument6,
argument7);

##空行
一段代码的语义和另一段代码不相关,这时就应该使用空格将它们分隔开来。

##命名

  • 常量
    常量使用大写字母和下划线命名。
1
2
var MAX_COUNT = 10;
var URL = 'http://www.google.com/';
  • 普通变量和函数
    遵守驼峰大小写命名法。变量以名词作为前缀,函数以动词作为前缀。
1
2
3
4
5
var myName = 'Tom';

function getName() {
return myName;
}

动词常见约定:
动词常见约定

  • 构造函数
    构造函数首字母应该大写。
1
2
3
4
5
function Person(name) {
this.name = name;
}

var me = Person('Tom');

##基本类型的表示

  • 字符串
    普通字符串采用'单引号包裹,json 对象中的字符串采用双引号包裹。
1
2
3
4
5
6
var name = 'Tom';

var me = {
name: "Tom",
age: 23
}
  • 数字
    书写完整,不要省去整数部分或小数部分,不要使用八进制写法。
  • null
    不要使用null来检查是否传入了某个参数;不要用null来检查一个未初始化的变量。
  • undefined
    尽量避免在代码中使用undefined,如果要判断一个变量是否存在可以这样:
1
2
3
if(name === null || typeof(name) === 'undefined') {
// ..
}
  • 对象
    采用对象字面量的方式定义对象:
1
2
3
4
var me = {
name: "Tom",
age: 23
}
  • 数组
    采用数组字面量的方式定义数组:
1
var colors = ['red', 'green', 'blue'];

CSS3 Animate or JS Animate ?

发表于 2015-05-27   |   分类于 css3   |  

既然 CSS3 和 jquery 都可以实现动画, 那么到底哪中方式的执行效率更高呢,我们简单的来比较一下吧。

首先考虑一下,改变 DOM 样式导致浏览器重绘有两种方式,一种是单纯的重绘,页面布局没有受到影响(如改变盒子的背景色,修改字体颜色,修改透明度…);另外一种是在重绘之前会触发 Rendering(回流),也就是页面布局受到了影响,需要重新布局,在重绘页面。下面我们分别对这两种情况进行测试。

测试重绘

写了两个 DEMO,都是改变 div 的背景色:

  • CSS3 实现
  • jqeury 实现

测试结果如下(CSS3 左, jquery 右):

使用 jquery 动画效果图

由于 jquery 中,脚本运行占据了一定的CPU 时间,而 css3 几乎全部开销在页面重绘上。 这样看来 css3 具有更好的性能。

测试回流+重绘

接下来这两个 DEMO 效果是移动 DIV 的位置,这会导致浏览器触发 Rendering(回流),然后再进行重绘:

  • CSS3 实现
  • jquery 实现

测试结果如下(CSS3 左, jquery 右):

使用CSS3动画效果图

可以看到,两者没有太打的区别。

结论

通过上面两个简单的测试,可以得出一个结论,CSS3 同 jquery 在处理元素几何属性发生变化的动画时,表现相当;但是在处理元素特性变换时 CSS3 的性能要好。

CSS3 动画库

可以通过Animate.css 动画库获取各种动画特效,并且可以自定义需要的动画效果列表。用 CSS3 动画来代替 jquery UI 可以大大的提升网页性能,如果你不打算考虑IE9-, 呵呵。

动画库测试 DEMO:http://codepen.io/CodingMonkeyzh/pen/gpLdPW

推荐阅读

  • 「译」高效的 “box-shadow” 动画
1…3456
McBird

McBird

分享自己的学习经历,实用技巧

27 日志
13 分类
25 标签
GitHub 微博 知乎
© 2015 - 2021 McBird
由 Hexo 强力驱动
主题 - NexT.Muse