console
class MarkdownParser {
compile(nodes) {
let matchArr
let html = ''
for (let i = 0, len = nodes.length; i < len; i++) {
let text = ''
let index = 0
const node = nodes[i]
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.innerHTML === '<br>') {
// 多个空行只算一个
html += `<div></div>`
while (nodes[i + 1].nodeType === Node.ELEMENT_NODE && nodes[i + 1].innerHTML === '<br>') {
i++
}
continue
}
index = node.dataset.index
text = node.textContent.trim()
} else {
text = node.nodeValue.trim()
}
matchArr = text.match(/^#\s/)
|| text.match(/^##\s/)
|| text.match(/^###\s/)
|| text.match(/^####\s/)
|| text.match(/^#####\s/)
|| text.match(/^######\s/)
|| text.match(/^\*{3,}/)
|| text.match(/^>/)
|| text.match(/^\*\s/)
|| text.match(/^\d*\.\s/)
|| text.match(/^```/)
|| text.match(/^\|.*\|/)
if (matchArr) {
let temp = ''
const re1 = /^>/
const re2 = /^\*\s/
const re3 = /^\d*\.\s/
const re4 = /^```/
const re5 = /^\|.*\|/
switch(matchArr[0]) {
case '# ':
html += `<h1 data-index="${index}">` + this.format(text.substring(2)) + '</h1>'
break
case '## ':
html += `<h2 data-index="${index}">` + this.format(text.substring(3)) + '</h2>'
break
case '### ':
html += `<h3 data-index="${index}">` + this.format(text.substring(4)) + '</h3>'
break
case '#### ':
html += `<h4 data-index="${index}">` + this.format(text.substring(5)) + '</h4>'
break
case '##### ':
html += `<h5 data-index="${index}">` + this.format(text.substring(6)) + '</h5>'
break
case '###### ':
html += `<h6 data-index="${index}">` + this.format(text.substring(7)) + '</h6>'
break
case text.match(/^\*{3,}/) && text.match(/^\*{3,}/)[0]:
html += text.replace(/^\*{3,}/g, '<hr>')
break
case '>':
while (i < len && nodes[i].textContent.match(re1)) {
const str = nodes[i].textContent
temp += '<div>' + this.format(str.slice(1)) + '</div>'
i++
}
i--
html += `<blockquote data-index="${index}">` + temp + '</blockquote>'
break
case '* ':
while (i < len && nodes[i].textContent?.match(re2)) {
const str = nodes[i].textContent
temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.slice(2)) + '</li>'
i++
}
i--
html += `<ul data-index="${index}">` + temp + '</ul>'
break
case text.match(/^\d*\.\s/) && text.match(/^\d*\.\s/)[0]:
while (i < len && nodes[i].textContent?.match(re3)) {
const str = nodes[i].textContent
temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.replace(/^\d*\.\s/, '')) + '</li>'
i++
}
i--
html += `<ol data-index="${index}">` + temp + '</ol>'
break
case '```':
i++
while (i < len && !re4.test(nodes[i].textContent)) {
temp += `<div data-index="${nodes[i]?.dataset?.index}">` + escapeHTML(nodes[i].textContent) + '</div>'
i++
}
html += `<pre data-index="${index}"><code>` + temp + '</code></pre>'
break
case text.match(/^\|.*\|/) && text.match(/^\|.*\|/)[0]:
let thRe = /^\[th\]/
let arr, j, jlen
while (i < len && re5.test(nodes[i].textContent)) {
arr = nodes[i].textContent.split('|')
temp += `<tr data-index="${nodes[i]?.dataset?.index}">`
for (j = 1, jlen = arr.length - 1; j < jlen; j++) {
if (thRe.test(arr[1])) {
temp += '<th>' + arr[j] + '</th>'
} else {
temp += '<td>' + arr[j] + '</td>'
}
}
temp += '</tr>'
temp = temp.replace('[th]', '')
i++
}
html += '<table>' + temp + '</table>'
break
}
} else if (text) {
html += `<div data-index="${index}">` + this.format(text) + '</div>'
}
}
return html
}
format(str) {
str = str.replace(/\s/g, ' ')
const bold = str.match(/\*{2}[^*].*?\*{2}/g) // 惰性匹配
if (bold) {
for (let i = 0, len = bold.length; i < len; i++) {
str = str.replace(bold[i], '<b>' + bold[i].substring(2, bold[i].length - 2) + '</b>')
}
}
const italic = str.match(/\*[^*].*?\*/g)
if (italic) {
for (let i = 0, len = italic.length; i < len; i++) {
str = str.replace(italic[i], '<i>' + italic[i].substring(1, italic[i].length - 1) + '</i>')
}
}
const code = str.match(/`[^`]*`/g)
if (code) {
for (let i = 0, len = code.length; i < len; i++) {
str = str.replace(code[i], '<code>' + code[i].substring(1, code[i].length - 1) + '</code>')
}
}
const img = str.match(/!\[.*\]\(.*\)/g)
const re1 = /\(.*\)/
const re2 = /\[.*\]/
if (img) {
for (let i = 0, len = img.length; i < len; i++) {
const url = img[i].match(re1)[0]
const title = img[i].match(re2)[0]
str = str.replace(img[i], '<img src=' + url.substring(1, url.length - 1) + ' alt=' + title.substring(1, title.length -1) + '>')
}
}
const a = str.match(/\[.*?\]\(.*?\)/g)
if (a) {
for (let i = 0, len = a.length; i < len; i++) {
const url = a[i].match(re1)[0]
const title = a[i].match(re2)[0]
str = str.replace(a[i], '<a href=' + url.substring(1, url.length - 1) + '>' + title.substring(1, title.length -1) + '</a>')
}
}
return escapeHTML2(str)
}
}
function escapeHTML(html) {
return html.replace(/</g, '<').replace(/>/g, '>')
}
function escapeHTML2(html) {
return html.replace(/<(\/)?script>/g, '<$1script>')
}
function $(selector) {
return document.querySelector(selector)
}
// 修改光标位置
function changeCursorPosition(node) {
const selection = window.getSelection()
// 清除所有选区 如果是 Caret 类型,清除选区后变为 Range,如果不是 Range 类型,后面的 addRange() 就不起作用
selection.removeAllRanges()
const range = document.createRange()
// 选中节点的内容
range.selectNode(node)
selection.addRange(range)
// 取消选中并将光标移至选区最后
selection.collapseToEnd()
}
// 清除复制后的内容样式
function clearTextStyle(e) {
e.preventDefault()
let text
const clp = (e.originalEvent || e).clipboardData
if (clp === undefined || clp === null) {
text = window.clipboardData.getData('text') || ''
if (text !== '') {
if (window.getSelection) {
var newNode = document.createElement('span')
newNode.innerHTML = text
window.getSelection().getRangeAt(0).insertNode(newNode)
} else {
document.selection.createRange().pasteHTML(text)
}
}
} else {
text = clp.getData('text/plain') || ''
if (text !== '') {
document.execCommand('insertText', false, text)
}
}
}
function throttle(delay) {
let waitForCallFunc
let canCall = true
return function helper(callback, ...args) {
if (!canCall) {
if (callback) waitForCallFunc = callback
return
}
callback(...args)
canCall = false
setTimeout(() => {
canCall = true
if (waitForCallFunc) {
helper(waitForCallFunc, ...args)
waitForCallFunc = null
}
}, delay)
}
}
function debounce(delay) {
let timer
return function(callback, ...args) {
clearTimeout(timer)
timer = setTimeout(() => callback.call(null, ...args), delay)
}
}
/**
* 计算 dom 到容器顶部的距离
* @param {HTMLElement} dom 需要计算的容器
* @param {HTMLElement} topContainer 终止条件
* @returns
*/
function getHeightToTop(dom) {
let height = dom.offsetTop
let parent = dom.offsetParent
while (parent) {
height += parent.offsetTop
parent = parent.offsetParent
}
return height
}
// dom 是否在屏幕内
function isInScreen(dom) {
const { top, bottom } = dom.getBoundingClientRect()
return bottom >= 0 && top < window.innerHeight
}
// dom 在当前屏幕展示内容的百分比
function percentOfdomInScreen(dom) {
const { height, bottom } = dom.getBoundingClientRect()
if (bottom <= 0) return 0
if (bottom >= height) return 1
return bottom / height
}
function canNodeCalculate(node) {
return (
node.innerHTML
&& node.innerHTML !== '<br>'
&& !node.textContent.startsWith('```')
&& isInScreen(node)
&& percentOfdomInScreen(node) >= 0
)
}
const editor = $('#editor')
const showDom = $('#show-content')
const markdown = new MarkdownParser()
showDom.innerHTML = markdown.compile(editor.children)
function onInput() {
// 为每个元素加上索引,通过索引找到 markdown 渲染后的元素
let index = 0
const data = Array.from(editor.children)
data.forEach(item => {
delete item.dataset.index
// 忽略 br 换行符和空文本字节
if (item.tagName !== 'BR' && item.innerText.trim() !== '') {
if (!item.children.length || (item.children.length === 1 && item.children[0].tagName === 'BR')) {
item.dataset.index = index++
return
}
// 这里主要是针对复制过来的有嵌套节点的内容
const frag = document.createDocumentFragment()
Array.from(item.childNodes).forEach(e => {
if (e.nodeType === Node.TEXT_NODE) {
const div = document.createElement('div')
div.textContent = e.nodeValue
item.replaceChild(div, e)
div.dataset.index = index++
frag.appendChild(div)
} else if (item.tagName !== 'BR') {
e.dataset?.index && delete e.dataset.index
e.dataset.index = index++
frag.appendChild(e)
}
})
editor.replaceChild(frag, item)
// 需要修改光标位置,不然光标会在复制内容的前面,修改后会在复制内容的后面
changeCursorPosition(editor.querySelector(`[data-index="${index - 1}"]`))
}
})
showDom.innerHTML = markdown.compile(editor.childNodes)
}
const debounceFn = debounce(100) // 防抖
editor.oninput = () => {
debounceFn(onInput)
}
editor.onpaste = (e) => {
clearTextStyle(e)
}
// 是否允许滚动
const canScroll = {
editor: true,
showDom: true,
}
const debounceFn2 = debounce(100) // 防抖
const throttleFn = throttle(50) // 节流
editor.onscroll = () => {
if (!canScroll.editor) return
canScroll.showDom = false
throttleFn(onScroll, editor, showDom)
debounceFn2(resumeScroll)
}
showDom.onscroll = () => {
if (!canScroll.showDom) return
canScroll.editor = false
throttleFn(onScroll, showDom, editor)
debounceFn(resumeScroll)
}
// 恢复滚动
function resumeScroll() {
canScroll.editor = true
canScroll.showDom = true
}
/**
*
* @param {HTMLElement} scrollContainer 正在滚动的容器
* @param {HTMLElement} ShowContainer 需要同步滚动的容器
* @returns
*/
function onScroll(scrollContainer, ShowContainer) {
const scrollHeight = ShowContainer.scrollHeight
// 滚动到底部
if (scrollContainer.offsetHeight + scrollContainer.scrollTop >= scrollContainer.scrollHeight) {
ShowContainer.scrollTo({ top: scrollHeight - ShowContainer.clientHeight })
return
}
// 滚动到顶部
if (scrollContainer.scrollTop === 0) {
ShowContainer.scrollTo({ top: 0 })
return
}
const nodes = Array.from(scrollContainer.children)
for (const node of nodes) {
// 从上往下遍历,找到第一个在屏幕内的元素
if (canNodeCalculate(node)) {
// 如果当前滚动的元素是 <pre> <table>
if (node.tagName === 'PRE' || node.tagName === 'TABLE') {
// 如果 pre 里面的子元素同步滚动了,则直接返回
if (hasPreElementInScrollContainerScroll(node, ShowContainer)) return
// 否则直接从下一个元素开始计算
continue
}
const index = node.dataset.index
const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
if (!dom) continue
const percent = percentOfdomInScreen(node)
const heightToTop = getHeightToTop(dom)
const domNeedHideHeight = dom.offsetHeight * (1 - percent)
ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
break
}
}
}
function hasPreElementInScrollContainerScroll(preElement, ShowContainer) {
for (const node of preElement.children[0].children) {
// 从上往下遍历,找到第一个在屏幕内的元素
if (isInScreen(node) && percentOfdomInScreen(node) >= 0) {
const index = node.dataset.index
const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
if (!dom) continue
const percent = percentOfdomInScreen(node)
const heightToTop = getHeightToTop(dom)
const domNeedHideHeight = dom.offsetHeight * (1 - percent)
ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
return true
}
}
return false
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>markdown 编辑器双屏同步滚动 demo2</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="editor" contenteditable class="text-container">
<div><br></div><div data-index="0">性能优化是把双刃剑,有好的一面也有坏的一面。好的一面就是能提升网站性能,坏的一面就是配置麻烦,或者要遵守的规则太多。并且某些性能优化规则并不适用所有场景,需要谨慎使用,请读者带着批判性的眼光来阅读本文。</div><div><br></div><div data-index="1">本文相关的优化建议的引用资料出处均会在建议后面给出,或者放在文末。</div><div><br></div><div data-index="2">### 1. 减少 HTTP 请求</div><div data-index="3">一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。接下来看一个具体的例子帮助理解 HTTP :</div><div><br></div><div data-index="4"></div><div><br></div><div data-index="5">这是一个 HTTP 请求,请求的文件大小为 28.4KB。</div><div><br></div><div data-index="6">名词解释:</div><div data-index="7">* Queueing: 在请求队列中的时间。</div><div data-index="8">* Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。</div><div data-index="9">* Proxy negotiation: 与代理服务器连接进行协商所花费的时间。</div><div data-index="10">* DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。</div><div data-index="11">* Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。</div><div data-index="12">* SSL: 完成SSL握手所花费的时间。</div><div data-index="13">* Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。</div><div data-index="14">* Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间。</div><div data-index="15">* Content Download: 接收响应数据所花费的时间。</div><div><br></div><div data-index="16">从这个例子可以看出,真正下载数据的时间占比为 `13.05 / 204.16 = 6.39%`,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。</div><div><br></div><div data-index="17">参考资料:</div><div data-index="18">* [understanding-resource-timing](https://developers.google.com/web/tools/chrome-devtools/network/understanding-resource-timing)</div><div><br></div><div data-index="19">### 2. 使用 HTTP2</div><div data-index="20">HTTP2 相比 HTTP1.1 有如下几个优点:</div><div data-index="21">#### 解析速度快</div><div data-index="22">服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。</div><div data-index="23">#### 多路复用</div><div data-index="24">HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。</div><div><br></div><div data-index="25">在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。</div><div data-index="26">多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。</div><div><br></div><div data-index="27">#### 首部压缩</div><div data-index="28">HTTP2 提供了首部压缩功能。</div><div><br></div><div data-index="29">例如有如下两个请求:</div><div data-index="30">```</div><div data-index="31">:authority: unpkg.zhimg.com</div><div data-index="32">:method: GET</div><div data-index="33">:path: /za-js-sdk@2.16.0/dist/zap.js</div><div data-index="34">:scheme: https</div><div data-index="35">accept: */*</div><div data-index="36">accept-encoding: gzip, deflate, br</div><div data-index="37">accept-language: zh-CN,zh;q=0.9</div><div data-index="38">cache-control: no-cache</div><div data-index="39">pragma: no-cache</div><div data-index="40">referer: https://www.zhihu.com/</div><div data-index="41">sec-fetch-dest: script</div><div data-index="42">sec-fetch-mode: no-cors</div><div data-index="43">sec-fetch-site: cross-site</div><div data-index="44">user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36</div><div data-index="45">```</div><div data-index="46">```</div><div data-index="47">:authority: zz.bdstatic.com</div><div data-index="48">:method: GET</div><div data-index="49">:path: /linksubmit/push.js</div><div data-index="50">:scheme: https</div><div data-index="51">accept: */*</div><div data-index="52">accept-encoding: gzip, deflate, br</div><div data-index="53">accept-language: zh-CN,zh;q=0.9</div><div data-index="54">cache-control: no-cache</div><div data-index="55">pragma: no-cache</div><div data-index="56">referer: https://www.zhihu.com/</div><div data-index="57">sec-fetch-dest: script</div><div data-index="58">sec-fetch-mode: no-cors</div><div data-index="59">sec-fetch-site: cross-site</div><div data-index="60">user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36</div><div data-index="61">```</div><div data-index="62">从上面两个请求可以看出来,有很多数据都是重复的。如果可以把相同的首部存储起来,仅发送它们之间不同的部分,就可以节省不少的流量,加快请求的时间。</div><div><br></div><div data-index="63">HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。</div><div><br></div><div data-index="64">下面再来看一个简化的例子,假设客户端按顺序发送如下请求首部:</div><div data-index="65">```</div><div data-index="66">Header1:foo</div><div data-index="67">Header2:bar</div><div data-index="68">Header3:bat</div><div data-index="69">```</div><div data-index="70">当客户端发送请求时,它会根据首部值创建一张表:</div><div><br></div><div data-index="71">|索引|首部名称|值|</div><div data-index="72">|62|Header1|foo|</div><div data-index="73">|63|Header2|bar|</div><div data-index="74">|64|Header3|bat|</div><div><br></div><div data-index="75">如果服务器收到了请求,它会照样创建一张表。</div><div data-index="76">当客户端发送下一个请求的时候,如果首部相同,它可以直接发送这样的首部块:</div><div data-index="77">```</div><div data-index="78">62 63 64</div><div data-index="79">```</div><div data-index="80">服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。</div><div><br></div><div data-index="81">#### 优先级</div><div data-index="82">HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。</div><div><br></div><div data-index="83">#### 流量控制</div><div data-index="84">由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。</div><div><br></div><div data-index="85">#### 服务器推送</div><div data-index="86">HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。</div><div><br></div><div data-index="87">例如当浏览器请求一个网站时,除了返回 HTML 页面外,服务器还可以根据 HTML 页面中的资源的 URL,来提前推送资源。</div><div><br></div><div><br></div><div data-index="88">现在有很多网站已经开始使用 HTTP2 了,例如知乎:</div><div><br></div><div data-index="89"></div><div><br></div><div data-index="90">其中 h2 是指 HTTP2 协议,http/1.1 则是指 HTTP1.1 协议。</div><div><br></div><div data-index="91">参考资料:</div><div data-index="92">* [HTTP2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)</div><div data-index="93">* [半小时搞懂 HTTP、HTTPS和HTTP2](https://github.com/woai3c/Front-end-articles/blob/master/http-https-http2.md)</div><div><br></div><div><br></div><div data-index="94">### 3. 使用服务端渲染</div><div data-index="95">客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。</div><div><br></div><div data-index="96">服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。</div><div><br></div><div data-index="97">* 优点:首屏渲染快,SEO 好。</div><div data-index="98">* 缺点:配置麻烦,增加了服务器的计算压力。</div><div><br></div><div data-index="99">参考资料:</div><div data-index="100">* [vue-ssr-demo](https://github.com/woai3c/vue-ssr-demo)</div><div data-index="101">* [Vue.js 服务器端渲染指南](https://ssr.vuejs.org/zh/)</div><div><br></div><div data-index="102">### 4. 静态资源使用 CDN</div><div data-index="103">内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。</div><div><br></div><div data-index="104">#### CDN 原理</div><div data-index="105">当用户访问一个网站时,如果没有 CDN,过程是这样的:</div><div data-index="106">1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。</div><div data-index="107">2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到网站服务器的 IP 地址。</div><div data-index="108">3. 本地 DNS 将 IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并得到资源。</div><div><br></div><div data-index="109"></div><div><br></div><div data-index="110">如果用户访问的网站部署了 CDN,过程是这样的:</div><div data-index="111">1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。</div><div data-index="112">2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址。</div><div data-index="113">3. 本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS。</div><div data-index="114">4. 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。</div><div data-index="115">5. SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。</div><div data-index="116">6. 浏览器再根据 SLB 发回的地址重定向到缓存服务器。</div><div data-index="117">7. 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。</div><div><br></div><div data-index="118"></div><div><br></div><div data-index="119">参考资料:</div><div data-index="120">* [CDN是什么?使用CDN有什么优势?](https://www.zhihu.com/question/36514327/answer/193768864)</div><div data-index="121">* [CDN原理简析](https://juejin.im/post/6844903873518239752)</div><div><br></div><div data-index="122">### 5. 将 CSS 放在文件头部,JavaScript 文件放在底部</div><div data-index="123">* CSS 执行会阻塞渲染,阻止 JS 执行</div><div data-index="124">* JS 加载和执行会阻塞 HTML 解析,阻止 CSSOM 构建</div><div><br></div><div data-index="125">如果这些 CSS、JS 标签放在 HEAD 标签里,并且需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部(不阻止 DOM 解析,但会阻塞渲染),等 HTML 解析完了再加载 JS 文件,尽早向用户呈现页面的内容。</div><div><br></div><div data-index="126">那为什么 CSS 文件还要放在头部呢?</div><div><br></div><div data-index="127">因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。</div><div><br></div><div data-index="128">另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。</div><div><br></div><div data-index="129">参考资料:</div><div data-index="130">* [使用 JavaScript 添加交互](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/adding-interactivity-with-javascript)</div><div data-index="131">### 6. 使用字体图标 iconfont 代替图片图标</div><div data-index="132">字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。</div><div><br></div><div data-index="133">#### 压缩字体文件</div><div data-index="134">使用 [fontmin-webpack](https://github.com/patrickhulce/fontmin-webpack) 插件对字体文件进行压缩(感谢[前端小伟](https://juejin.im/user/237150239985165)提供)。</div><div><br></div><div data-index="135"></div><div><br></div><div data-index="136">参考资料:</div><div data-index="137">* [fontmin-webpack](https://github.com/patrickhulce/fontmin-webpack)</div><div data-index="138">* [Iconfont-阿里巴巴矢量图标库](https://www.iconfont.cn/)</div><div><br></div><div data-index="139">### 7. 善用缓存,不重复加载相同的资源</div><div data-index="140">为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。</div><div><br></div><div data-index="141">不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件?</div><div><br></div><div data-index="142">可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。</div><div><br></div><div data-index="143">具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用[数据摘要要算法](https://baike.baidu.com/item/%E6%B6%88%E6%81%AF%E6%91%98%E8%A6%81%E7%AE%97%E6%B3%95/3286770?fromtitle=%E6%91%98%E8%A6%81%E7%AE%97%E6%B3%95&fromid=12011257)对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。</div><div><br></div><div data-index="144">参考资料:</div><div data-index="145">* [webpack + express 实现文件精确缓存](https://github.com/woai3c/node-blog/blob/master/doc/node-blog7.md)</div><div data-index="146">* [webpack-缓存](https://www.webpackjs.com/guides/caching/)</div><div data-index="147">* [张云龙--大公司里怎样开发和部署前端代码?](https://www.zhihu.com/question/20790576/answer/32602154)</div><div><br></div><div><br></div><div data-index="148">### 8. 压缩文件</div><div data-index="149">压缩文件可以减少文件下载时间,让用户体验性更好。</div><div><br></div><div data-index="150">得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。</div><div><br></div><div data-index="151">在 webpack 可以使用如下插件进行压缩:</div><div data-index="152">* JavaScript:UglifyPlugin</div><div data-index="153">* CSS :MiniCssExtractPlugin</div><div data-index="154">* HTML:HtmlWebpackPlugin</div><div><br></div><div data-index="155">其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。</div><div><br></div><div data-index="156">gzip 是目前最流行和最有效的压缩方法。举个例子,我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。</div><div><br></div><div data-index="157">附上 webpack 和 node 配置 gzip 的使用方法。</div><div><br></div><div data-index="158">**下载插件**</div><div data-index="159">```</div><div data-index="160">npm install compression-webpack-plugin --save-dev</div><div data-index="161">npm install compression</div><div data-index="162">```</div><div><br></div><div data-index="163">**webpack 配置**</div><div data-index="164">```</div><div data-index="165">const CompressionPlugin = require('compression-webpack-plugin');</div><div><br></div><div data-index="166">module.exports = {</div><div data-index="167"> plugins: [new CompressionPlugin()],</div><div data-index="168">}</div><div data-index="169">```</div><div><br></div><div data-index="170">**node 配置**</div><div data-index="171">```</div><div data-index="172">const compression = require('compression')</div><div data-index="173">// 在其他中间件前使用</div><div data-index="174">app.use(compression())</div><div data-index="175">```</div><div><br></div><div data-index="176">### 9. 图片优化</div><div data-index="177">#### (1). 图片延迟加载</div><div data-index="178">在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。</div><div><br></div><div data-index="179">首先可以将图片这样设置,在页面不可见时图片不会加载:</div><div data-index="180">```html</div><div data-index="181"><img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4"></div><div data-index="182">```</div><div data-index="183">等页面可见时,使用 JS 加载图片:</div><div data-index="184">```js</div><div data-index="185">const img = document.querySelector('img')</div><div data-index="186">img.src = img.dataset.src</div><div data-index="187">```</div><div data-index="188">这样图片就加载出来了,完整的代码可以看一下参考资料。</div><div><br></div><div data-index="189">参考资料:</div><div data-index="190">* [web 前端图片懒加载实现原理](https://juejin.im/entry/6844903482164510734)</div><div><br></div><div data-index="191">#### (2). 响应式图片</div><div data-index="192">响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。</div><div><br></div><div data-index="193">通过 `picture` 实现</div><div data-index="194">```html</div><div data-index="195"><picture></div><div data-index="196"> <source srcset="banner_w1000.jpg" media="(min-width: 801px)"></div><div data-index="197"> <source srcset="banner_w800.jpg" media="(max-width: 800px)"></div><div data-index="198"> <img src="banner_w800.jpg" alt=""></div><div data-index="199"></picture></div><div data-index="200">```</div><div data-index="201">通过 `@media` 实现</div><div data-index="202">```html</div><div data-index="203">@media (min-width: 769px) {</div><div data-index="204"> .bg {</div><div data-index="205"> background-image: url(bg1080.jpg);</div><div data-index="206"> }</div><div data-index="207">}</div><div data-index="208">@media (max-width: 768px) {</div><div data-index="209"> .bg {</div><div data-index="210"> background-image: url(bg768.jpg);</div><div data-index="211"> }</div><div data-index="212">}</div><div data-index="213">```</div><div data-index="214">#### (3). 调整图片大小</div><div data-index="215">例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。</div><div><br></div><div data-index="216">所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。</div><div><br></div><div data-index="217">#### (4). 降低图片质量</div><div data-index="218">例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。</div><div><br></div><div data-index="219">压缩方法有两种,一是通过 webpack 插件 `image-webpack-loader`,二是通过在线网站进行压缩。</div><div><br></div><div data-index="220">以下附上 webpack 插件 `image-webpack-loader` 的用法。</div><div data-index="221">```</div><div data-index="222">npm i -D image-webpack-loader</div><div data-index="223">```</div><div data-index="224">webpack 配置</div><div data-index="225">```js</div><div data-index="226">{</div><div data-index="227"> test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,</div><div data-index="228"> use:[</div><div data-index="229"> {</div><div data-index="230"> loader: 'url-loader',</div><div data-index="231"> options: {</div><div data-index="232"> limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/</div><div data-index="233"> name: utils.assetsPath('img/[name].[hash:7].[ext]')</div><div data-index="234"> }</div><div data-index="235"> },</div><div data-index="236"> /*对图片进行压缩*/</div><div data-index="237"> {</div><div data-index="238"> loader: 'image-webpack-loader',</div><div data-index="239"> options: {</div><div data-index="240"> bypassOnDebug: true,</div><div data-index="241"> }</div><div data-index="242"> }</div><div data-index="243"> ]</div><div data-index="244">}</div><div data-index="245">```</div><div><br></div><div data-index="246">#### (5). 尽可能利用 CSS3 效果代替图片</div><div data-index="247">有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。</div><div><br></div><div data-index="248">参考资料:</div><div data-index="249">* [img图片在webpack中使用](https://juejin.im/post/6844903816081457159)</div><div><br></div><div data-index="250">#### (6). 使用 webp 格式的图片</div><div data-index="251">>WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。</div><div><br></div><div data-index="252">参考资料:</div><div data-index="253">* [WebP 相对于 PNG、JPG 有什么优势?](https://www.zhihu.com/question/27201061)</div><div><br></div><div data-index="254">### 10. 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码</div><div data-index="255">>懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。</div><div><br></div><div data-index="256">#### 根据文件内容生成文件名,结合 import 动态引入组件实现按需加载</div><div data-index="257">通过配置 output 的 filename 属性可以实现这个需求。filename 属性的值选项中有一个 [contenthash],它将根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 也会发生变化。</div><div data-index="258">```js</div><div data-index="259">output: {</div><div data-index="260"> filename: '[name].[contenthash].js',</div><div data-index="261"> chunkFilename: '[name].[contenthash].js',</div><div data-index="262"> path: path.resolve(__dirname, '../dist'),</div><div data-index="263">},</div><div data-index="264">```</div><div data-index="265">#### 提取第三方库</div><div data-index="266">由于引入的第三方库一般都比较稳定,不会经常改变。所以将它们单独提取出来,作为长期缓存是一个更好的选择。</div><div data-index="267">这里需要使用 webpack4 的 splitChunk 插件 cacheGroups 选项。</div><div data-index="268">```js</div><div data-index="269">optimization: {</div><div data-index="270"> runtimeChunk: {</div><div data-index="271"> name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个单独的 chunk。</div><div data-index="272"> },</div><div data-index="273"> splitChunks: {</div><div data-index="274"> cacheGroups: {</div><div data-index="275"> vendor: {</div><div data-index="276"> name: 'chunk-vendors',</div><div data-index="277"> test: /[\\/]node_modules[\\/]/,</div><div data-index="278"> priority: -10,</div><div data-index="279"> chunks: 'initial'</div><div data-index="280"> },</div><div data-index="281"> common: {</div><div data-index="282"> name: 'chunk-common',</div><div data-index="283"> minChunks: 2,</div><div data-index="284"> priority: -20,</div><div data-index="285"> chunks: 'initial',</div><div data-index="286"> reuseExistingChunk: true</div><div data-index="287"> }</div><div data-index="288"> },</div><div data-index="289"> }</div><div data-index="290">},</div><div data-index="291">```</div><div data-index="292">* test: 用于控制哪些模块被这个缓存组匹配到。原封不动传递出去的话,它默认会选择所有的模块。可以传递的值类型:RegExp、String和Function;</div><div data-index="293">* priority:表示抽取权重,数字越大表示优先级越高。因为一个 module 可能会满足多个 cacheGroups 的条件,那么抽取到哪个就由权重最高的说了算;</div><div data-index="294">* reuseExistingChunk:表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。</div><div data-index="295">* minChunks(默认是1):在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割)</div><div data-index="296">* chunks (默认是async) :initial、async和all</div><div data-index="297">* name(打包的chunks的名字):字符串或者函数(函数可以根据条件自定义名字)</div><div><br></div><div data-index="298">#### 减少 ES6 转为 ES5 的冗余代码</div><div data-index="299">Babel 转化后的代码想要实现和原来代码一样的功能需要借助一些帮助函数,比如:</div><div data-index="300">```js</div><div data-index="301">class Person {}</div><div data-index="302">```</div><div data-index="303">会被转换为:</div><div data-index="304">```js</div><div data-index="305">"use strict";</div><div><br></div><div data-index="306">function _classCallCheck(instance, Constructor) {</div><div data-index="307"> if (!(instance instanceof Constructor)) {</div><div data-index="308"> throw new TypeError("Cannot call a class as a function");</div><div data-index="309"> }</div><div data-index="310">}</div><div><br></div><div data-index="311">var Person = function Person() {</div><div data-index="312"> _classCallCheck(this, Person);</div><div data-index="313">};</div><div data-index="314">```</div><div data-index="315">这里 `_classCallCheck` 就是一个 `helper` 函数,如果在很多文件里都声明了类,那么就会产生很多个这样的 `helper` 函数。</div><div><br></div><div data-index="316">这里的 `@babel/runtime` 包就声明了所有需要用到的帮助函数,而 `@babel/plugin-transform-runtime` 的作用就是将所有需要 `helper` 函数的文件,从 `@babel/runtime包` 引进来:</div><div data-index="317">```js</div><div data-index="318">"use strict";</div><div><br></div><div data-index="319">var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");</div><div><br></div><div data-index="320">var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);</div><div><br></div><div data-index="321">function _interopRequireDefault(obj) {</div><div data-index="322"> return obj && obj.__esModule ? obj : { default: obj };</div><div data-index="323">}</div><div><br></div><div data-index="324">var Person = function Person() {</div><div data-index="325"> (0, _classCallCheck3.default)(this, Person);</div><div data-index="326">};</div><div data-index="327">```</div><div data-index="328">这里就没有再编译出 `helper` 函数 `classCallCheck` 了,而是直接引用了 `@babel/runtime` 中的 `helpers/classCallCheck`。</div><div><br></div><div data-index="329">**安装**</div><div data-index="330">```</div><div data-index="331">npm i -D @babel/plugin-transform-runtime @babel/runtime</div><div data-index="332">```</div><div data-index="333">**使用**</div><div data-index="334">在 `.babelrc` 文件中</div><div data-index="335">```</div><div data-index="336">"plugins": [</div><div data-index="337"> "@babel/plugin-transform-runtime"</div><div data-index="338">]</div><div data-index="339">```</div><div><br></div><div data-index="340">参考资料:</div><div data-index="341">* [Babel 7.1介绍 transform-runtime polyfill env](https://www.jianshu.com/p/d078b5f3036a)</div><div data-index="342">* [懒加载](http://webpack.docschina.org/guides/lazy-loading/)</div><div data-index="343">* [Vue 路由懒加载](https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E8%B7%AF%E7%94%B1%E6%87%92%E5%8A%A0%E8%BD%BD)</div><div data-index="344">* [webpack 缓存](https://webpack.docschina.org/guides/caching/)</div><div data-index="345">* [一步一步的了解webpack4的splitChunk插件](https://juejin.im/post/6844903614759043079)</div><div><br></div><div><br></div><div data-index="346">### 11. 减少重绘重排</div><div data-index="347">**浏览器渲染过程**</div><div data-index="348">1. 解析HTML生成DOM树。</div><div data-index="349">2. 解析CSS生成CSSOM规则树。</div><div data-index="350">3. 将DOM树与CSSOM规则树合并在一起生成渲染树。</div><div data-index="351">4. 遍历渲染树开始布局,计算每个节点的位置大小信息。</div><div data-index="352">5. 将渲染树每个节点绘制到屏幕。</div><div><br></div><div data-index="353"></div><div><br></div><div data-index="354">**重排**</div><div><br></div><div data-index="355">当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。</div><div><br></div><div data-index="356">**重绘**</div><div><br></div><div data-index="357">当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排 。</div><div><br></div><div data-index="358">重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。</div><div><br></div><div data-index="359">什么操作会导致重排?</div><div data-index="360">* 添加或删除可见的 DOM 元素</div><div data-index="361">* 元素位置改变</div><div data-index="362">* 元素尺寸改变</div><div data-index="363">* 内容改变</div><div data-index="364">* 浏览器窗口尺寸改变</div><div><br></div><div data-index="365">如何减少重排重绘?</div><div data-index="366">* 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。</div><div data-index="367">* 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。</div><div><br></div><div data-index="368">### 12. 使用事件委托</div><div data-index="369">事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。</div><div data-index="370">```js</div><div data-index="371"><ul></div><div data-index="372"> <li>苹果</li></div><div data-index="373"> <li>香蕉</li></div><div data-index="374"> <li>凤梨</li></div><div data-index="375"></ul></div><div><br></div><div data-index="376">// good</div><div data-index="377">document.querySelector('ul').onclick = (event) => {</div><div data-index="378"> const target = event.target</div><div data-index="379"> if (target.nodeName === 'LI') {</div><div data-index="380"> console.log(target.innerHTML)</div><div data-index="381"> }</div><div data-index="382">}</div><div><br></div><div data-index="383">// bad</div><div data-index="384">document.querySelectorAll('li').forEach((e) => {</div><div data-index="385"> e.onclick = function() {</div><div data-index="386"> console.log(this.innerHTML)</div><div data-index="387"> }</div><div data-index="388">}) </div><div data-index="389">```</div><div><br></div><div data-index="390">### 13. 注意程序的局部性</div><div data-index="391">一个编写良好的计算机程序常常具有良好的局部性,它们倾向于引用最近引用过的数据项附近的数据项,或者最近引用过的数据项本身,这种倾向性,被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。</div><div><br></div><div data-index="392">**局部性通常有两种不同的形式:**</div><div data-index="393">* 时间局部性:在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来被多次引用。</div><div data-index="394">* 空间局部性 :在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。</div><div><br></div><div data-index="395">时间局部性示例</div><div data-index="396">```js</div><div data-index="397">function sum(arry) {</div><div data-index="398"> let i, sum = 0</div><div data-index="399"> let len = arry.length</div><div><br></div><div data-index="400"> for (i = 0; i < len; i++) {</div><div data-index="401"> sum += arry[i]</div><div data-index="402"> }</div><div><br></div><div data-index="403"> return sum</div><div data-index="404">}</div><div data-index="405">```</div><div data-index="406">在这个例子中,变量sum在每次循环迭代中被引用一次,因此,对于sum来说,具有良好的时间局部性</div><div><br></div><div data-index="407">空间局部性示例</div><div><br></div><div data-index="408">**具有良好空间局部性的程序**</div><div data-index="409">```js</div><div data-index="410">// 二维数组 </div><div data-index="411">function sum1(arry, rows, cols) {</div><div data-index="412"> let i, j, sum = 0</div><div><br></div><div data-index="413"> for (i = 0; i < rows; i++) {</div><div data-index="414"> for (j = 0; j < cols; j++) {</div><div data-index="415"> sum += arry[i][j]</div><div data-index="416"> }</div><div data-index="417"> }</div><div data-index="418"> return sum</div><div data-index="419">}</div><div data-index="420">```</div><div data-index="421">**空间局部性差的程序**</div><div data-index="422">```js</div><div data-index="423">// 二维数组 </div><div data-index="424">function sum2(arry, rows, cols) {</div><div data-index="425"> let i, j, sum = 0</div><div><br></div><div data-index="426"> for (j = 0; j < cols; j++) {</div><div data-index="427"> for (i = 0; i < rows; i++) {</div><div data-index="428"> sum += arry[i][j]</div><div data-index="429"> }</div><div data-index="430"> }</div><div data-index="431"> return sum</div><div data-index="432">}</div><div data-index="433">```</div><div data-index="434">看一下上面的两个空间局部性示例,像示例中从每行开始按顺序访问数组每个元素的方式,称为具有步长为1的引用模式。</div><div data-index="435">如果在数组中,每隔k个元素进行访问,就称为步长为k的引用模式。</div><div data-index="436">一般而言,随着步长的增加,空间局部性下降。</div><div><br></div><div data-index="437">这两个例子有什么区别?区别在于第一个示例是按行扫描数组,每扫描完一行再去扫下一行;第二个示例是按列来扫描数组,扫完一行中的一个元素,马上就去扫下一行中的同一列元素。</div><div><br></div><div data-index="438">数组在内存中是按照行顺序来存放的,结果就是逐行扫描数组的示例得到了步长为 1 引用模式,具有良好的空间局部性;而另一个示例步长为 rows,空间局部性极差。</div><div><br></div><div data-index="439">#### 性能测试</div><div data-index="440">运行环境:</div><div data-index="441">* cpu: i5-7400 </div><div data-index="442">* 浏览器: chrome 70.0.3538.110</div><div><br></div><div data-index="443">对一个长度为9000的二维数组(子数组长度也为9000)进行10次空间局部性测试,时间(毫秒)取平均值,结果如下:<br></div><div><br></div><div data-index="444">所用示例为上述两个空间局部性示例</div><div><br></div><div data-index="445">|步长为 1|步长为 9000|</div><div data-index="446">|124|2316|</div><div><br></div><div data-index="447">从以上测试结果来看,步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。</div><div><br></div><div data-index="448">总结:</div><div data-index="449">* 重复引用相同变量的程序具有良好的时间局部性</div><div data-index="450">* 对于具有步长为 k 的引用模式的程序,步长越小,空间局部性越好;而在内存中以大步长跳来跳去的程序空间局部性会很差</div><div><br></div><div data-index="451">参考资料:</div><div data-index="452">* [深入理解计算机系统](https://book.douban.com/subject/26912767/)</div><div><br></div><div><br></div><div data-index="453">### 14. if-else 对比 switch</div><div data-index="454">当判断条件数量越来越多时,越倾向于使用 switch 而不是 if-else。</div><div data-index="455">```js</div><div data-index="456">if (color == 'blue') {</div><div><br></div><div data-index="457">} else if (color == 'yellow') {</div><div><br></div><div data-index="458">} else if (color == 'white') {</div><div><br></div><div data-index="459">} else if (color == 'black') {</div><div><br></div><div data-index="460">} else if (color == 'green') {</div><div><br></div><div data-index="461">} else if (color == 'orange') {</div><div><br></div><div data-index="462">} else if (color == 'pink') {</div><div><br></div><div data-index="463">}</div><div><br></div><div data-index="464">switch (color) {</div><div data-index="465"> case 'blue':</div><div><br></div><div data-index="466"> break</div><div data-index="467"> case 'yellow':</div><div><br></div><div data-index="468"> break</div><div data-index="469"> case 'white':</div><div><br></div><div data-index="470"> break</div><div data-index="471"> case 'black':</div><div><br></div><div data-index="472"> break</div><div data-index="473"> case 'green':</div><div><br></div><div data-index="474"> break</div><div data-index="475"> case 'orange':</div><div><br></div><div data-index="476"> break</div><div data-index="477"> case 'pink':</div><div><br></div><div data-index="478"> break</div><div data-index="479">}</div><div data-index="480">```</div><div data-index="481">像上面的这种情况,从可读性来说,使用 switch 是比较好的(js 的 switch 语句不是基于哈希实现,而是循环判断,所以说 if-else、switch 从性能上来说是一样的)。</div><div><br></div><div data-index="482">### 15. 查找表</div><div data-index="483">当条件语句特别多时,使用 switch 和 if-else 不是最佳的选择,这时不妨试一下查找表。查找表可以使用数组和对象来构建。</div><div data-index="484">```js</div><div data-index="485">switch (index) {</div><div data-index="486"> case '0':</div><div data-index="487"> return result0</div><div data-index="488"> case '1':</div><div data-index="489"> return result1</div><div data-index="490"> case '2':</div><div data-index="491"> return result2</div><div data-index="492"> case '3':</div><div data-index="493"> return result3</div><div data-index="494"> case '4':</div><div data-index="495"> return result4</div><div data-index="496"> case '5':</div><div data-index="497"> return result5</div><div data-index="498"> case '6':</div><div data-index="499"> return result6</div><div data-index="500"> case '7':</div><div data-index="501"> return result7</div><div data-index="502"> case '8':</div><div data-index="503"> return result8</div><div data-index="504"> case '9':</div><div data-index="505"> return result9</div><div data-index="506"> case '10':</div><div data-index="507"> return result10</div><div data-index="508"> case '11':</div><div data-index="509"> return result11</div><div data-index="510">}</div><div data-index="511">```</div><div data-index="512">可以将这个 switch 语句转换为查找表</div><div data-index="513">```js</div><div data-index="514">const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]</div><div><br></div><div data-index="515">return results[index]</div><div data-index="516">```</div><div data-index="517">如果条件语句不是数值而是字符串,可以用对象来建立查找表</div><div data-index="518">```js</div><div data-index="519">const map = {</div><div data-index="520"> red: result0,</div><div data-index="521"> green: result1,</div><div data-index="522">}</div><div><br></div><div data-index="523">return map[color]</div><div data-index="524">```</div><div><br></div><div data-index="525">### 16. 避免页面卡顿</div><div data-index="526">**60fps 与设备刷新率**</div><div data-index="527">>目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。</div><div data-index="528">其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。</div><div><br></div><div data-index="529"></div><div><br></div><div data-index="530">假如你用 JavaScript 修改了 DOM,并触发样式修改,经历重排重绘最后画到屏幕上。如果这其中任意一项的执行时间过长,都会导致渲染这一帧的时间过长,平均帧率就会下降。假设这一帧花了 50 ms,那么此时的帧率为 1s / 50ms = 20fps,页面看起来就像卡顿了一样。</div><div><br></div><div data-index="531">对于一些长时间运行的 JavaScript,我们可以使用定时器进行切分,延迟执行。</div><div data-index="532">```js</div><div data-index="533">for (let i = 0, len = arry.length; i < len; i++) {</div><div data-index="534"> process(arry[i])</div><div data-index="535">}</div><div data-index="536">```</div><div data-index="537">假设上面的循环结构由于 process() 复杂度过高或数组元素太多,甚至两者都有,可以尝试一下切分。</div><div data-index="538">```js</div><div data-index="539">const todo = arry.concat()</div><div data-index="540">setTimeout(function() {</div><div data-index="541"> process(todo.shift())</div><div data-index="542"> if (todo.length) {</div><div data-index="543"> setTimeout(arguments.callee, 25)</div><div data-index="544"> } else {</div><div data-index="545"> callback(arry)</div><div data-index="546"> }</div><div data-index="547">}, 25)</div><div data-index="548">```</div><div data-index="549">如果有兴趣了解更多,可以查看一下[高性能JavaScript](https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/%E9%AB%98%E6%80%A7%E8%83%BDJavaScript.pdf)第 6 章和[高效前端:Web高效编程与优化实践](https://book.douban.com/subject/30170670/)第 3 章。</div><div><br></div><div data-index="550">参考资料:</div><div data-index="551">* [渲染性能](https://developers.google.com/web/fundamentals/performance/rendering)</div><div><br></div><div><br></div><div data-index="552">### 17. 使用 requestAnimationFrame 来实现视觉变化</div><div data-index="553">从第 16 点我们可以知道,大多数设备屏幕刷新率为 60 次/秒,也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候,最好的情况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的唯一方式是使用 `requestAnimationFrame`。</div><div data-index="554">```js</div><div data-index="555">/**</div><div data-index="556"> * If run as a requestAnimationFrame callback, this</div><div data-index="557"> * will be run at the start of the frame.</div><div data-index="558"> */</div><div data-index="559">function updateScreen(time) {</div><div data-index="560"> // Make visual updates here.</div><div data-index="561">}</div><div><br></div><div data-index="562">requestAnimationFrame(updateScreen);</div><div data-index="563">```</div><div data-index="564">如果采取 `setTimeout` 或 `setInterval` 来实现动画的话,回调函数将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。</div><div><br></div><div data-index="565"></div><div><br></div><div data-index="566">参考资料:</div><div data-index="567">* [优化 JavaScript 执行](https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution?hl=zh-cn)</div><div><br></div><div data-index="568">### 18. 使用 Web Workers</div><div data-index="569">Web Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序(反之亦然)。</div><div><br></div><div data-index="570">Web Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。</div><div><br></div><div data-index="571">创建一个新的 worker 很简单,指定一个脚本的 URI 来执行 worker 线程(main.js):</div><div data-index="572">```js</div><div data-index="573">var myWorker = new Worker('worker.js');</div><div data-index="574">// 你可以通过postMessage() 方法和onmessage事件向worker发送消息。</div><div data-index="575">first.onchange = function() {</div><div data-index="576"> myWorker.postMessage([first.value,second.value]);</div><div data-index="577"> console.log('Message posted to worker');</div><div data-index="578">}</div><div><br></div><div data-index="579">second.onchange = function() {</div><div data-index="580"> myWorker.postMessage([first.value,second.value]);</div><div data-index="581"> console.log('Message posted to worker');</div><div data-index="582">}</div><div data-index="583">```</div><div data-index="584">在 worker 中接收到消息后,我们可以写一个事件处理函数代码作为响应(worker.js):</div><div data-index="585">```js</div><div data-index="586">onmessage = function(e) {</div><div data-index="587"> console.log('Message received from main script');</div><div data-index="588"> var workerResult = 'Result: ' + (e.data[0] * e.data[1]);</div><div data-index="589"> console.log('Posting message back to main script');</div><div data-index="590"> postMessage(workerResult);</div><div data-index="591">}</div><div data-index="592">```</div><div data-index="593">onmessage处理函数在接收到消息后马上执行,代码中消息本身作为事件的data属性进行使用。这里我们简单的对这2个数字作乘法处理并再次使用postMessage()方法,将结果回传给主线程。</div><div><br></div><div data-index="594">回到主线程,我们再次使用onmessage以响应worker回传的消息:</div><div data-index="595">```js</div><div data-index="596">myWorker.onmessage = function(e) {</div><div data-index="597"> result.textContent = e.data;</div><div data-index="598"> console.log('Message received from worker');</div><div data-index="599">}</div><div data-index="600">```</div><div data-index="601">在这里我们获取消息事件的data,并且将它设置为result的textContent,所以用户可以直接看到运算的结果。</div><div><br></div><div data-index="602">不过在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。</div><div><br></div><div data-index="603">参考资料:</div><div data-index="604">* [Web Workers](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers)</div><div><br></div><div data-index="605">### 19. 使用位操作</div><div data-index="606">JavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。但是在位操作中,数字被转换为有符号的 32 位格式。即使需要转换,位操作也比其他数学运算和布尔操作快得多。</div><div><br></div><div data-index="607">##### 取模</div><div data-index="608">由于偶数的最低位为 0,奇数为 1,所以取模运算可以用位操作来代替。</div><div data-index="609">```js</div><div data-index="610">if (value % 2) {</div><div data-index="611"> // 奇数</div><div data-index="612">} else {</div><div data-index="613"> // 偶数 </div><div data-index="614">}</div><div data-index="615">// 位操作</div><div data-index="616">if (value & 1) {</div><div data-index="617"> // 奇数</div><div data-index="618">} else {</div><div data-index="619"> // 偶数</div><div data-index="620">}</div><div data-index="621">```</div><div data-index="622">##### 取整</div><div data-index="623">```js</div><div data-index="624">~~10.12 // 10</div><div data-index="625">~~10 // 10</div><div data-index="626">~~'1.5' // 1</div><div data-index="627">~~undefined // 0</div><div data-index="628">~~null // 0</div><div data-index="629">```</div><div data-index="630">##### 位掩码</div><div data-index="631">```js</div><div data-index="632">const a = 1</div><div data-index="633">const b = 2</div><div data-index="634">const c = 4</div><div data-index="635">const options = a | b | c</div><div data-index="636">```</div><div data-index="637">通过定义这些选项,可以用按位与操作来判断 a/b/c 是否在 options 中。</div><div data-index="638">```js</div><div data-index="639">// 选项 b 是否在选项中</div><div data-index="640">if (b & options) {</div><div data-index="641"> ...</div><div data-index="642">}</div><div data-index="643">```</div><div><br></div><div data-index="644">### 20. 不要覆盖原生方法</div><div data-index="645">无论你的 JavaScript 代码如何优化,都比不上原生方法。因为原生方法是用低级语言写的(C/C++),并且被编译成机器码,成为浏览器的一部分。当原生方法可用时,尽量使用它们,特别是数学运算和 DOM 操作。</div><div><br></div><div data-index="646">### 21. 降低 CSS 选择器的复杂性</div><div data-index="647">#### (1). 浏览器读取选择器,遵循的原则是从选择器的右边到左边读取。</div><div><br></div><div data-index="648">看个示例</div><div data-index="649">```css</div><div data-index="650">#block .text p {</div><div data-index="651"> color: red;</div><div data-index="652">}</div><div data-index="653">```</div><div data-index="654">1. 查找所有 P 元素。</div><div data-index="655">2. 查找结果 1 中的元素是否有类名为 text 的父元素</div><div data-index="656">3. 查找结果 2 中的元素是否有 id 为 block 的父元素</div><div><br></div><div data-index="657">#### (2). CSS 选择器优先级</div><div data-index="658">```</div><div data-index="659">内联 > ID选择器 > 类选择器 > 标签选择器</div><div data-index="660">```</div><div data-index="661">根据以上两个信息可以得出结论。</div><div data-index="662">1. 选择器越短越好。</div><div data-index="663">2. 尽量使用高优先级的选择器,例如 ID 和类选择器。</div><div data-index="664">3. 避免使用通配符 *。 </div><div><br></div><div data-index="665">最后要说一句,据我查找的资料所得,CSS 选择器没有优化的必要,因为最慢和慢快的选择器性能差别非常小。</div><div><br></div><div data-index="666">参考资料:</div><div data-index="667">* [CSS selector performance](https://ecss.io/appendix1.html)</div><div data-index="668">* [Optimizing CSS: ID Selectors and Other Myths](https://www.sitepoint.com/optimizing-css-id-selectors-and-other-myths/)</div><div><br></div><div><br></div><div data-index="669">### 22. 使用 flexbox 而不是较早的布局模型</div><div data-index="670">在早期的 CSS 布局方式中我们能对元素实行绝对定位、相对定位或浮动定位。而现在,我们有了新的布局方式 [flexbox](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox),它比起早期的布局方式来说有个优势,那就是性能比较好。</div><div><br></div><div data-index="671">下面的截图显示了在 1300 个框上使用浮动的布局开销:</div><div><br></div><div data-index="672"></div><div><br></div><div data-index="673">然后我们用 flexbox 来重现这个例子:</div><div><br></div><div data-index="674"></div><div><br></div><div data-index="675">现在,对于相同数量的元素和相同的视觉外观,布局的时间要少得多(本例中为分别 3.5 毫秒和 14 毫秒)。</div><div><br></div><div data-index="676">不过 flexbox 兼容性还是有点问题,不是所有浏览器都支持它,所以要谨慎使用。</div><div><br></div><div data-index="677">各浏览器兼容性:</div><div data-index="678">* Chrome 29+</div><div data-index="679">* Firefox 28+</div><div data-index="680">* Internet Explorer 11</div><div data-index="681">* Opera 17+</div><div data-index="682">* Safari 6.1+ (prefixed with -webkit-)</div><div data-index="683">* Android 4.4+</div><div data-index="684">* iOS 7.1+ (prefixed with -webkit-)</div><div><br></div><div><br></div><div data-index="685">参考资料:</div><div data-index="686">* [使用 flexbox 而不是较早的布局模型](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing?hl=zh-cn)</div><div><br></div><div data-index="687">### 23. 使用 transform 和 opacity 属性更改来实现动画</div><div data-index="688">在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。</div><div><br></div><div data-index="689"></div><div><br></div><div data-index="690">参考资料:</div><div data-index="691">* [使用 transform 和 opacity 属性更改来实现动画](https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count?hl=zh-cn)</div><div><br></div><div data-index="692">### 24. 合理使用规则,避免过度优化</div><div data-index="693">性能优化主要分为两类:</div><div data-index="694">1. 加载时优化</div><div data-index="695">2. 运行时优化</div><div><br></div><div data-index="696">上述 23 条建议中,属于加载时优化的是前面 10 条建议,属于运行时优化的是后面 13 条建议。通常来说,没有必要 23 条性能优化规则都用上,根据网站用户群体来做针对性的调整是最好的,节省精力,节省时间。</div><div><br></div><div data-index="697">在解决问题之前,得先找出问题,否则无从下手。所以在做性能优化之前,最好先调查一下网站的加载性能和运行性能。</div><div><br></div><div data-index="698">##### 检查加载性能</div><div data-index="699">一个网站加载性能如何主要看白屏时间和首屏时间。</div><div data-index="700">* 白屏时间:指从输入网址,到页面开始显示内容的时间。</div><div data-index="701">* 首屏时间:指从输入网址,到页面完全渲染的时间。</div><div><br></div><div data-index="702">将以下脚本放在 `</head>` 前面就能获取白屏时间。</div><div data-index="703">```html</div><div data-index="704"><script></div><div data-index="705"> new Date() - performance.timing.navigationStart</div><div data-index="706"> // 通过 domLoading 和 navigationStart 也可以</div><div data-index="707"> performance.timing.domLoading - performance.timing.navigationStart</div><div data-index="708"></script></div><div data-index="709">```</div><div><br></div><div data-index="710">在 `window.onload` 事件里执行 `new Date() - performance.timing.navigationStart` 即可获取首屏时间。 </div><div><br></div><div data-index="711">##### 检查运行性能</div><div><br></div><div data-index="712">配合 chrome 的开发者工具,我们可以查看网站在运行时的性能。</div><div><br></div><div data-index="713">打开网站,按 F12 选择 performance,点击左上角的灰色圆点,变成红色就代表开始记录了。这时可以模仿用户使用网站,在使用完毕后,点击 stop,然后你就能看到网站运行期间的性能报告。如果有红色的块,代表有掉帧的情况;如果是绿色,则代表 FPS 很好。performance 的具体使用方法请用搜索引擎搜索一下,毕竟篇幅有限。</div><div><br></div><div data-index="714">通过检查加载和运行性能,相信你对网站性能已经有了大概了解。所以这时候要做的事情,就是使用上述 23 条建议尽情地去优化你的网站,加油!</div><div><br></div><div data-index="715">参考资料:</div><div data-index="716">* [performance.timing.navigationStart](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming/navigationStart)</div><div data-index="717">## 其他参考资料</div><div data-index="718">* [性能为何至关重要](https://developers.google.com/web/fundamentals/performance/why-performance-matters?hl=zh-cn)</div><div data-index="719">* [高性能网站建设指南](https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%AB%99%E5%BB%BA%E8%AE%BE%E6%8C%87%E5%8D%97.pdf)</div><div data-index="720">* [Web性能权威指南](https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/Web%E6%80%A7%E8%83%BD%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97.pdf)</div><div data-index="721">* [高性能JavaScript](https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/%E9%AB%98%E6%80%A7%E8%83%BDJavaScript.pdf)</div><div data-index="722">* [高效前端:Web高效编程与优化实践](https://book.douban.com/subject/30170670/)</div><div><br></div><div data-index="723">### [更多文章,欢迎关注](https://zhuanlan.zhihu.com/c_1056848825012023296)</div>
</div>
<div id="show-content" class="text-container markdown"></div>
</body>
</html>
html,
body {
height: 100%;
font-size: 0;
}
* {
margin: 0;
padding: 0;
font-family: 'Microsoft yahei';
}
body {
display: flex;
}
table {
border-collapse: collapse;
}
.text-container {
box-sizing: border-box;
width: 50%;
border: 1px solid #e6e6e6;
height: 100%;
font-size: 14px;
background: #f5f5f5;
padding: 10px;
line-height: 20px;
outline: none;
overflow: auto;
}
.text-container > div {
min-height: 20px;
}
/* 定义滚动条高宽及背景
高宽分别对应横竖滚动条的尺寸 */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
/* 定义滚动条轨道
内阴影+圆角 */
::-webkit-scrollbar-track {
background-color: transparent;
}
/* 定义滑块
内阴影+圆角 */
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #ddd;
}
/* markdown style */
.markdown {
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
background-color: #fff;
color: #24292f;
}
.markdown img {
max-width: 100%;
margin: 10px 0;
}
.markdown>:first-child {
margin-top: 0!important;
}
.markdown h1,
.markdown h2 {
border-bottom: 1px solid #eaecef;
padding-bottom: .3em;
}
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
font-weight: 600;
line-height: 1.25;
margin-bottom: 16px;
margin-top: 24px;
}
.markdown div {
min-height: 20px;
}
code,
pre,
tt {
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
font-size: 14px;
}
.markdown pre {
background-color: #f6f8fa;
border-radius: 3px;
font-size: 85%;
line-height: 1.45;
overflow: auto;
padding: 16px;
color: #24292e;
font-size: 14px;
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
margin: 0;
margin-bottom: 1em;
}
.markdown code {
background-color: #f6f8fa;
border: 0;
display: inline;
line-height: inherit;
margin: 0;
max-width: auto;
overflow: visible;
padding: 0;
word-wrap: normal;
word-break: break-all;
}
.markdown blockquote {
background-color: #f6f8fa;
padding: 10px;
}
.markdown table {
display: block;
overflow: auto;
width: 100%;
margin-bottom: 15px;
}
.markdown table tr {
background-color: #fff;
}
.markdown table td,
.markdown table th {
border: 1px solid #dfe2e5;
padding: 6px 13px;
}
.markdown p {
margin-bottom: 1em;
}
.markdown li {
word-wrap: break-all;
}
.markdown ul li {
list-style: disc;
}
.markdown ol,
.markdown ul {
padding-left: 2em;
}
.markdown blockquote,
.markdown details,
.markdown dl,
.markdown ol,
.markdown p,
.markdown pre,
.markdown table,
.markdown ul {
margin-bottom: 16px;
margin-top: 0;
}
.markdown a {
color: #0366d6;
text-decoration: none;
}