## 一、语言
**1)慎用全局变量**
  当变量暴露在全局作用域中时,由于全局作用域比较复杂,因此查找会比较慢。
  并且还有可能污染window对象,覆盖之前所赋的值,发生意想不到的错误。
~~~
0 == '' //true
0 == '0' //true
~~~
**3)简写**
  简写的方式很多,此处只会列举其中的几种,例如用三目运算替代if-else语句,或用&&或||符号替代条件语句。
~~~
if (count > 1) {
++a;
} else {
--a;
}
// 简写
count > 1 ? (++a) : (--a);
if (count) {
++a;
}
// 简写
count && (++a)
~~~
  利用ES6语法,可以用解构赋值,简洁明了。还有些小技巧包括用箭头函数表示回调,块级作用域变量等。
~~~
const { count } = obj;
~~~
  关于去除数组中的重复数,可以采用ES6最新的Set数据结构。
~~~
Array.from(new Set(arr));
~~~
**4)减少魔法数**
  魔法数是指意义不明的常量,例如直接在代码中使用一个数字1,其判断条件令人费解。
~~~
if(type == 1) { }
~~~
  而如果将该数字赋给一个语义化的常量后,就能明确其意图。
~~~
const ERROR_TYPE = 1;
if(type == ERROR_TYPE) { }
~~~
**5)位运算**
  用位运算取代纯数学操作,例如对2取模(digit%2)判断偶数与奇数。
~~~
if (digit & 1) {
// 奇数(odd)
} else {
// 偶数(even)
}
~~~
  位掩码技术,使用单个数字的每一位来判断选项是否成立。掩码中每个选项的值都是2的幂。
~~~
var OPTION_A = 1, OPTION_B = 2, OPTION_C = 4, OPTION_D = 8, OPTION_E = 16;
//用按位或运算创建一个数字来包含多个设置选项
var options = OPTION_A | OPTION_C | OPTION_D;
//接下来可以用按位与操作来判断给定的选项是否可用
//选项A是否在列表中
if(options & OPTION_A) {
//...
}
~~~
  用按位左移(\<<)做乘法,用按位右移做除法(\>>),例如digit*2可以替换成digit<<2。
**6)字符串拼接**
  除了使用加号(+)或加等(+=)实现字符串拼接之外,还可以使用数组的join()和字符串的concat()方法。
~~~
["strick", "jane"].join("");
"strick".concat("jane");
~~~
  ES6提供的模板字面量是一种能够嵌入表达式的格式化字符串,也可以用来做字符串拼接。
~~~
str = "My name is \"" + name + "\". M y age is " + age + "."; //传统拼接方式
str = `My name is "${name}". My age is ${age}.`; //模板字面量方式
~~~
**7)正则优化**
  正则优化包括:
1. 减少分支数量,缩小分支范围;
2. 使用非捕获数组;
3. 只捕获感兴趣的文本以减少后期处理;
4. 使用合适的量词;
5. 化繁为简,分解复杂的正则。
**8)惰性模式**
  惰性模式用于减少每次代码执行时的重复性分支判断,通过对对象重定义来屏蔽原对象中的分支判断。
  惰性模式分为两种:第一种文件加载后立即执行对象方法来重定义,第二种是当第一次使用方法对象时来重定义。
~~~
var A = {};
//加载时 损失性能 第一次加载时 不损失性能
A.on = (function (dom, type, fn) {
if (dom.addEventListener) {
return function (dom, type, fn) {
dom.addEventListener(type, fn, false);
};
} else if (dom.attachEvent) {
return function (dom, type, fn) {
dom.attachEvent("on" + type, fn);
};
} else {
return function (dom, type, fn) {
dom["on" + type] = fn;
};
}
})();
//加载时 不损失性能 第一次加载时 损失性能
A.on = function (dom, type, fn) {
if (dom.addEventListener) {
A.on = function (dom, type, fn) {
dom.addEventListener(type, fn, false);
};
} else if (dom.attachEvent) {
A.on = function (dom, type, fn) {
dom.attachEvent("on" + type, fn);
};
} else {
A.on = function (dom, type, fn) {
dom["on" + type] = fn;
};
}
//执行重定义on方法
A.on(dom, type, fn);
};
~~~
**9)使用缓存**
  当执行for循环时,需要读取数组的长度,可以事先做缓存。
~~~
for (let i = 0, len = arr.length; i < len; i++) {}
~~~
  或者在事件处理程序或对象方法中缓存this指向。
~~~
var obj = {
name: function () {
let self = this;
}
};
btn.addEventListener("click", function(event) {
let self = this;
}, false);
~~~
**10)记忆函数**
  记忆函数是指能够缓存先前计算结果的函数,避免重复执行不必要的复杂计算,是一种用空间换时间的编程技巧。
  具体的实施可以有多种写法,例如创建一个缓存对象,每次将计算条件作为对象的属性名,计算结果作为对象的属性值。
  下面的代码用于判断某个数是否是质数(质数又叫素数,是指一个大于1的自然数,除了1和它本身外,不能被其它自然数整除的数),在每次计算完成后,就将计算结果缓存到函数的自有属性digits内。
~~~
function prime(number) {
if (!prime.digits) {
prime.digits = {}; //缓存对象
}
if (prime.digits[number] !== undefined) {
return prime.digits[number];
}
var isPrime = false;
for (var i = 2; i < number; i++) {
if (number % i == 0) {
isPrime = false;
break;
}
}
if (i == number) {
isPrime = true;
}
return (prime.digits[number] = isPrime);
}
prime(87);
prime(17);
console.log(prime.digits[87]); //false
console.log(prime.digits[17]); //true
~~~
**11)闭包**
  通常来说,函数的活动对象会随着执行环境一同销毁。但引入闭包时,由于引用仍然存在于闭包的作用域中,因此对象无法被销毁。
~~~
function outter(count) {
count++;
// 闭包
function inner() {
return count + 1;
}
return inner();
}
~~~
  这意味着闭包需要更多的内存开销。在脚本编程中,要小心地使用闭包。
  推荐将跨作用域的变量存储到一个局部变量中,然后直接访问该局部变量,如下所示,将count作为参数传递给inner()函数。
~~~
function outter(count) {
count++;
// 闭包
function inner(count) {
return count + 1;
}
return inner(count);
}
~~~
**12)节流和去抖动**
  节流(throttle)是指预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。适用于mousemove事件、window对象的resize和scroll事件。
~~~
function throttle(fn, wait) {
let start = 0;
return () => {
const now = +new Date();
if (now - start > wait) {
fn();
start = now;
}
};
}
~~~
  去抖动(debounce)是指当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间。适用于文本输入的keydown事件,keyup事件,做autocomplete等。
~~~
function debounce(fn, wait) {
let start = null;
return () => {
clearTimeout(start);
start = setTimeout(fn, wait);
};
}
~~~
  节流与去抖动最大的不同的地方就是在计算最后执行时间的方式上。著名的开源工具库underscore中有内置了两个方法。
## 二、应用
**1)合理放置脚本**
  脚本会阻塞页面渲染,直至全部下载并执行完成后,页面渲染才会继续。浏览器在解析到body元素之前,不会渲染页面的任何部分。
  把脚本放在页面顶部会导致明显的延迟,通常表现为空白页面。因此推荐将所有script元素尽可能放到body元素底部。
**2)无阻塞脚本**
  为了解决阻塞的问题,script元素新增了两个布尔属性,分别是延迟(defer)和异步(async)。
1. defer:延迟脚本执行,直到文档解析完成。
2. async:尽快执行脚本,不会阻塞文档解析。
~~~html
<script src="scripts/jquery.js" defer></script>
<script src="scripts/jquery.js" async></script>
~~~
**3)动态脚本**
  用JavaScript动态创建script元素,文件的下载和执行过程不会阻塞页面其它进程。
~~~
var hm = document.createElement("script");
hm.src = "//www.pwstrick.com/hm.js";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
~~~
**4)图像上传**
  在上传图像时,可将其转换成Base64,相当于将图像做成字符串传送到后台。在下面的示例中用到了[FileReader](https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader)对象。
~~~
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(e) {
var img = new Image();
img.src = this.result;
console.log(this.result);
};
~~~
  注意,Base64图像会比原图要大。
**5)原生方法**
  JavaScript引擎提供的原生方法总是最快的。因为原生方法存在于浏览器中,并且都是用低级语言编写的。
  这意味着它们会被编译成机器码,成为浏览器的一部分,不会像自己写的JavaScript代码那样受到各种限制。
  CSS查询被JavaScript原生支持并被jQuery发扬光大。jQuery的选择器引擎虽然很快,但是仍然比原生方法慢。
  推荐使用原生的querySelector和querySelectorAll()作为选择器。
**6)本地缓存**
  在HTML5的本地缓存出现之前,都喜欢用cookie缓存数据。但cookie数据量只有4KB左右,并且每次都会携带在HTTP首部中,如果使用cookie保存过多数据会带来性能问题。
  而[localStorage](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage)和[sessionStorage](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/sessionStorage)数据量一般在2.5M到10M之间(大部分是5M),并且不参与和服务器之间的通信,因此比较容易实现网页或应用的离线化。
**7)重排和重绘**
  当DOM的变化影响了元素的几何属性(宽和高)将会发生重排(reflow),发生重排的情况如下所列。
1. 添加或删除可见的DOM元素
2. 元素位置改变
3. 元素尺寸改变(包括外边距、内边距、边框宽度、宽、高等属性)
4. 内容改变,例如文本改变或图片被不同尺寸的替换掉。
5. 页面渲染器初始化。
6. 浏览器窗口尺寸改变。
  完成重排后,浏览器会重新绘制受影响的部分到屏幕中,此过程为重绘(repaint)。
  下面代码看上去会重排3次,但其实只会重排1次,大多数浏览器通过队列化修改和批量显示优化重排版过程。
~~~
//渲染树变化的排队和刷新
var ele = document.getElementById('myDiv');
ele.style.borderLeft = '1px';
ele.style.borderRight = '2px';
ele.style.padding = '5px';
~~~
  但下列操作将会强迫队列刷新并要求所有计划改变的部分立刻应用:
~~~
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)(在 IE 中此函数称为 currentStyle)
~~~
  像offsetHeight属性需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并触发重排以返回正确的值。
  最小化重绘和重排的方式有两种:
1. cssText和class,cssText可以一次设置多个CSS属性。class也可以一次性设置,并且更清晰,更易于维护,但有前提条件,就是不依赖于运行逻辑和计算的情况。
2. 批量修改DOM,包括隐藏元素display:none,修改后重新显示display:block;使用文档片段fragment,在片段上操作节点,再拷贝回文档;将原始元素拷贝到一个脱离文档的节点中(例如position:absolute),修改副本,完成后再替换原始元素。
**8)定时器**
  为了不让一些复杂的JavaScript任务阻塞线程,就需要将其让出线程的控制权,即停止执行,可以通过定时器实现。
  当函数运行时间太长时,可以把它拆分成一系列更小的步骤,把每个独立的方法放到定时器中回调,如下所示,其中arguments.callee是指当前正在执行的函数。
~~~
let tasks = [openDocumnet, writeText, closeDocument, updateUI];
setTimeout(function() {
//执行下一个任务
let task = tasks.shift();
task();
//检查是否还有其他任务
if (tasks.length > 0) {
setTimeout(arguments.callee, 25);
}
}, 25);
~~~
**9)动画**
  JavaScript早期的动画是用定时器实现的,但随着浏览器功能的不断完善,出现了一种更新、性能更高的方法:[requestAnimationFrame()](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame)。
  requestAnimationFrame()会在重绘之前更新下一帧的动画,注意,回调函数自身必须再次调用requestAnimationFrame(),如下所示。
~~~
function step(timestamp) {
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
~~~
**10)Ajax**
  最快的Ajax请求是没有请求,即避免发送不必要的请求,例如:
1. 在服务端,设置HTTP首部信息以确保响应会被浏览器缓存。
2. 在客户端,把获取到的信息缓存到本地。
其它加速Ajax的技术包括:
1. 数据格式采用轻量级的JSON,解析速度快,通用性与XML相当。
2. 缩短页面加载时间,主要内容加载后,再用Ajax获取次要文件。
3. 确保代码的健壮性,错误不会输出给用户。
**11)DOMContentLoaded**
  当初始的HTML文档被完全加载和解析完成之后,[DOMContentLoaded](https://developer.mozilla.org/zh-CN/docs/Web/Events/DOMContentLoaded)事件被触发,而无需等待样式表、图像等资源的完全加载。
~~~
document.addEventListener("DOMContentLoaded", function() { }, false);
~~~
  另一个load事件应该仅用于检测一个完全加载的页面。
  注意,DOMContentLoaded事件必须等待其所属script之前的样式表加载解析完成后才会触发。
**12)事件委托**
  事件委托(event delegation)是一种提高程序性能、降低内存空间的技术手段,它利用了事件冒泡的特性,只需在某个祖先元素上注册一个事件,就能管理其所有后代元素上同一类型的事件。
  通过事件对象的target属性,就能分辨出当前运行在哪个事件目标上,如下所示。
~~~
container.addEventListener("click", function(event) {
event.target;
}, false);
~~~
  使用委托后就能避免对容器中的每个子元素注册事件,并且如果在容器中动态添加子元素,新加入的子元素也能使用容器元素上注册的事件,而不用再单独绑定一次事件处理程序。
**13)SSR**
  服务器端渲染([SSR](https://juejin.im/post/5def0816f265da33aa6aa7fe))是指将单页应用(SPA)在服务器端渲染成HTML片段,发送到浏览器,然后交由浏览器为其绑定状态与事件,成为完全可交互页面的过程。
其优点是:
1. 更快的首屏加载速度,无需等待JavaScript完成下载且执行之后才显示内容。
2. 更友好的SEO,爬虫可以直接抓取渲染之后的页面。
**14)MVVM**
  MVVM模式是指视图和数据之间的双向互通,视图的修改会反映给数据,反之亦然。
  目前市面上许多库和框架都会采用MVVM模式的思想,其提升的并不在于性能,而是开发效率,鼓励开发者操作数据更新视图,由库或框架最低限度的操作DOM,减少回流。
**15)虚拟DOM**
  虚拟DOM(Virtual DOM)是构建在真实DOM之上的一层抽象,它将DOM元素映射成内存中的JavaScript对象(即通过React.createElement()得到的React元素),形成一棵JavaScript对象树。
  虚拟DOM与模板引擎有些相似,将多次的DOM操作先在映射的JavaScript对象中处理,再将该对象一次性挂载到真实的DOM树上,避免因浏览器重排导致的大量无用计算。
  同构应用也是基于虚拟DOM实现的,虚拟DOM的思想还可应用于其它方面,例如[JavaScript录像回放](https://zhuanlan.zhihu.com/p/103253120)。
**16)帧**
  大部分情况下,生成一个新的帧都是由 JavaScript 通过修改 DOM 或者 CSSOM 来触发的。
  一个大的原则就是让单个帧的生成速度变快,优化策略如下。
1. 减少 JavaScript 脚本执行时间,不要一次霸占太久主线程,例如将执行的函数分解为多个任务。
2. 避免强制同步布局,即避免 JavaScript 强制将计算样式和布局操作提前到当前的任务中。
3. 避免布局抖动,即避免在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作。
4. 合理利用 CSS 合成动画,因为合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同。
5. 避免频繁的垃圾回收,要尽量避免产生那些临时垃圾数据和小颗粒对象的产生。
## 三、HTML5
**1)history**
  浏览器中的历史浏览记录就像一堆层叠的卡片,在HTML4中,可以使用window.history对象来控制历史记录的跳转。
  HTML5引进了history.pushState()方法和history.replaceState()方法,允许逐条地添加和修改历史记录条目。这些方法可以协同window.onpopstate事件一起工作。
  利用全新的history对象,就能让[Ajax](https://www.cnblogs.com/strick/p/4523995.html)就像重定向到新页面一样,拥有能够返回上一页或进入下一页的功能。
**2)Web Worker**
  Web Worker可以在主线程(通常是UI线程)之外运行代码,当在独立线程中执行费时的任务时,就能避免主线程被阻塞。
  注意,由于Web Worker没有绑定UI线程,因此它们不能访问浏览器的许多资源,例如从外部线程修改DOM会导致界面出现错误。
  由于Web Worker有着不同的全局运行环境,因此需要创建一个完全独立的JavaScript文件,其中包含了需要在Worker中运行的代码。
  例如下面的code.js,其中message事件用于接收信息,postMessage()方法用于发送信息。
~~~
var worker = new Worker("code.js");
worker.onmessage = function (event) {
console.log(event.data); //"hello strick"
};
worker.postMessage("strick");
// code.js的内部代码
self.onmessage = function (event) {
var text = `hello ${event.data}`;
self.postMessage(text);
};
~~~
  Worker通过importScripts()方法加载外部JavaScript文件,它的调用过程是阻塞式的,直到所有文件加载并执行完成之后,脚本才会继续运行。注意,不会影响UI响应。
~~~
importScripts("foo1.js", foo2.js);
~~~
  Web Worker的实际应用包括解析大JSON字符串,计算复杂数学运算(例如图像或视频处理),大数组排序,任何超过100ms的处理过程,都应该考虑Worker方案。
**2)Service Worker**
  Service Worker是谷歌发起的实现PWA(Progressive Web App,渐进式Web应用)的一个关键角色,它相当于Web应用与浏览器之间的一台代理服务器。
  Service Worker会在后台启动一条Worker线程(不能访问DOM),其工作是把一些资源缓存起来(跨域资源无法缓存),然后拦截页面的HTTPS请求,如果缓存中有,就从缓存里取,响应200,没有就走正常的请求流程。
  Service Worker结合Web App Manifest能完成离线使用、断网时返回200、将一个图标添加到桌面上等。
**3)WebAssembly**
  将繁重的计算(如Web游戏)任务抽离到WebAssembly(WASM)中,它是一种二进制指令格式,被设计为一种用高级语言(如C/C++/Rust)编译的可移植对象。
  WebAssembly的目的并不是替代JavaScript,而是与JavaScript共存,允许两者一起工作。
  通过使用WebAssembly的JavaScript接口,你可以把WebAssembly模块加载到一个JavaScript应用中,这样在同一个应用中就能同时享用WebAssembly的性能和JavaScript的灵活。
  下载一个[simple.wasm](https://raw.githubusercontent.com/mdn/webassembly-examples/master/js-api-examples/simple.wasm)示例,其内容如下所示。
~~~
(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i
)
)
~~~
  由于内部函数$i是从imports.imported\_func导入的,因此需要创建一个对象来反映simple.wasm中的两级命名空间。
~~~
let importObject = {
imports: {
imported_func: (arg) => console.log(arg)
}
};
~~~
  在加载wasm文件后,使其在Array Buffer中可用,然后就可以使用导出函数了。
~~~
fetch("simple.wasm")
.then((res) => res.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject))
.then((results) => {
results.instance.exports.exported_func();
});
~~~
*****
> 原文出处:
[博客园-Web优化躬行记](https://www.cnblogs.com/strick/category/1795726.html)
[知乎专栏-Web优化躬行记](https://zhuanlan.zhihu.com/c_1260996761008627712)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020