标签:计数器 终端 形式 模式 服务器 推荐 遇到 个数 拼接
参考:拉钩教育中
交互实现
meta 标签:自动刷新/跳转
假设要实现一个类似 PPT 自动播放的效果,你很可能会想到使用 JavaScript 定时器控制页面跳转来实现。但其实有更加简洁的实现方法,比如通过 meta 标签来实现:
<meta http-equiv="Refresh" content="5; URL=page2.html">
上面的代码会在 5s 之后自动跳转到同域下的 page2.html 页面。我们要实现 PPT 自动播放的功能,只需要在每个页面的 meta 标签内设置好下一个页面的地址即可。
另一种场景,比如每隔一分钟就需要刷新页面的大屏幕监控,也可以通过 meta 标签来实现,只需去掉后面的 URL 即可:
<meta http-equiv="Refresh" content="60">
既然这样做又方便又快捷,为什么这种用法比较少见呢?
title 标签与 Hack 手段:消息提醒
作为前端工程师的你对 B/S 架构肯定不陌生,它有很多的优点,比如版本更新方便、跨平台、跨终端,但在处理某些场景,比如即时通信场景时,就会变得比较麻烦。因为前后端通信深度依赖 HTTP 协议,而 HTTP 协议采用“请求-响应”模式,这就决定了服务端也只能被动地发送数据。一种低效的解决方案是客户端通过轮询机制获取最新消息(HTML5 下可使用 WebSocket 协议)。消息提醒功能实现则比较困难,HTML5 标准发布之前,浏览器没有开放图标闪烁、弹出系统消息之类的接口,只能借助一些 Hack 的手段,比如修改 title 标签来达到类似的效果(HTML5 下可使用 Web Notifications API 弹出系统消息)。
下面这段代码中,通过定时修改 title 标签内容,模拟了类似消息提醒的闪烁效果:
let msgNum = 1 // 消息条数 let cnt = 0 // 计数器 const inerval = setInterval(() => { cnt = (cnt + 1) % 2 if(msgNum===0) { // 通过DOM修改title document.title += `聊天页面` clearInterval(interval) return } const prefix = cnt % 2 ? `新消息(${msgNum})` : ‘‘ document.title = `${prefix}聊天页面` }, 1000)
实现效果如下图所示,可以看到标签名称上有提示文字在闪烁。
通过模拟消息闪烁,可以让用户在浏览其他页面的时候,及时得知服务端返回的消息。定时修改 title 标签内容,除了用来实现闪烁效果之外,还可以制作其他动画效果,比如文字滚动,但需要注意浏览器会对 title 标签文本进行去空格操作。动态修改 title 标签的用途不仅在于消息提醒,你还可以将一些关键信息显示到标签上(比如下载时的进度、当前操作步骤),从而提升用户体验。
性能优化
性能优化是前端开发中避不开的问题,性能问题无外乎两方面原因:渲染速度慢、请求时间长。性能优化虽然涉及很多复杂的原因和解决方案,但其实只要通过合理地使用标签,就可以在一定程度上提升渲染速度以及减少请求时间。
script 标签:调整加载顺序提升渲染速度
由于浏览器的底层运行机制,渲染引擎在解析 HTML 时,若遇到 script 标签引用文件,则会暂停解析过程,同时通知网络线程加载文件,文件加载后会切换至 JavaScript 引擎来执行对应代码,代码执行完成之后切换至渲染引擎继续渲染页面。在这一过程中可以看到,页面渲染过程中包含了请求文件以及执行文件的时间,但页面的首次渲染可能并不依赖这些文件,这些请求和执行文件的动作反而延长了用户看到页面的时间,从而降低了用户体验。
为了减少这些时间损耗,可以借助 script 标签的 3 个属性来实现。
具体效果可以参看下图:
其中,绿色的线表示执行解析 HTML ,蓝色的线表示请求文件,红色的线表示执行文件。
从图中可以得知,采用 3 种属性都能减少请求文件引起的阻塞时间,只有 defer 属性以及 type="module" 情况下能保证渲染引擎的优先执行,从而减少执行文件内容消耗的时间,让用户更快地看见页面(即使这些页面内容可能并没有完全地显示)。除此之外还应当注意,当渲染引擎解析 HTML 遇到 script 标签引入文件时,会立即进行一次渲染。所以这也就是为什么构建工具会把编译好的引用 JavaScript 代码的 script 标签放入到 body 标签底部,因为当渲染引擎执行到 body 底部时会先将已解析的内容渲染出来,然后再去请求相应的 JavaScript 文件。如果是内联脚本(即不通过 src 属性引用外部脚本文件直接在 HTML 编写 JavaScript 代码的形式),渲染引擎则不会渲染。
link 标签:通过预处理提升渲染速度
在我们对大型单页应用进行性能优化时,也许会用到按需懒加载的方式,来加载对应的模块,但如果能合理利用 link 标签的 rel 属性值来进行预加载,就能进一步提升渲染速度。
dns-prefetch。当 link 标签的 rel 属性值为“dns-prefetch”时,浏览器会对某个域名预先进行 DNS 解析并缓存。这样,当浏览器在请求同域名资源的时候,能省去从域名查询 IP 的过程,从而减少时间损耗。下图是淘宝网设置的 DNS 预解析。
preconnect。让浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析、TLS 协商、TCP 握手,通过消除往返延迟来为用户节省时间。
prefetch/preload。两个值都是让浏览器预先下载并缓存某个资源,但不同的是,prefetch 可能会在浏览器忙时被忽略,而 preload 则是一定会被预先下载。
prerender。浏览器不仅会加载资源,还会解析执行页面,进行预渲染。
这几个属性值恰好反映了浏览器获取资源文件的过程,在这里我绘制了一个流程简图,方便你记忆。
浏览器获取资源文件的流程
搜索优化
你所写的前端代码,除了要让浏览器更好执行,有时候也要考虑更方便其他程序(如搜索引擎)理解。合理地使用 meta 标签和 link 标签,恰好能让搜索引擎更好地理解和收录我们的页面。
meta 标签:提取关键信息
通过 meta 标签可以设置页面的描述信息,从而让搜索引擎更好地展示搜索结果。
例如,在百度中搜索“拉勾”,就会发现网站的描述信息,这些描述信息就是通过 meta 标签专门为搜索引擎设置的,目的是方便用户预览搜索到的结果。
link 标签:减少重复
有时候为了用户访问方便或者出于历史原因,对于同一个页面会有多个网址,又或者存在某些重定向页面,比如:
那么在这些页面中可以这样设置:
<link href="https://xx.com/a.html" rel="canonical">
这样可以让搜索引擎避免花费时间抓取重复网页。不过需要注意的是,它还有个限制条件,那就是指向的网站不允许跨域。
2.为什么说 DOM 操作耗时
1.线程切换:
如果你对浏览器结构有一定了解,就会知道浏览器包含渲染引擎(也称浏览器内核)和 JavaScript 引擎,它们都是单线程运行。单线程的优势是开发方便,避免多线程下的死锁、竞争等问题,劣势是失去了并发能力。
浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况,增加了另外一个机制,这两个引擎具有互斥性,也就是说在某个时刻只有一个引擎在运行,另一个引擎会被阻塞。操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息并读取下一个线程的状态信息,俗称上下文切换。而这个操作相对而言是比较耗时的。。
每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作,然后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗。单次切换消耗的时间是非常少的,但是如果频繁的大量切换,那么就会产生性能问题。
2.重新渲染
另一个更加耗时的因素是元素及样式变化引起的再次渲染,在渲染过程中最耗时的两个步骤为重排(Reflow)与重绘(Repaint)。
浏览器在渲染页面时会将 HTML 和 CSS 分别解析成 DOM 树和 CSSOM 树,然后合并进行排布,再绘制成我们可见的页面。如果在操作 DOM 时涉及到元素、样式的修改,就会引起渲染引擎重新计算样式生成 CSSOM 树,同时还有可能触发对元素的重新排布(简称“重排”)和重新绘制(简称“重绘”)。
可能会影响到其他元素排布的操作就会引起重排,继而引发重绘,比如:
与之相反的操作则只会引起重绘,比如:
如果想了解更多关于重绘和重排的样式属性,可以参看这个网址:https://csstriggers.com/。
下面是两段验证代码,我们通过 Chrome 提供的性能分析工具来对渲染耗时进行分析。
第一段代码,通过修改 div 元素的边距来触发重排,渲染耗时(粗略地认为渲染耗时为紫色 Rendering 事件和绿色 Painting 事件耗时之和)3045 毫秒。
const times = 100000 let html = ‘‘ for(let i=0;i<times;i++) { html+= `<div>${i}</div>` } document.body.innerHTML += html const divs = document.querySelectorAll(‘div‘) Array.prototype.forEach.call(divs, (div, i) => { div.style.margin = i % 2 ? ‘10px‘ : 0; })
第二段代码,修改 div 元素字体颜色来触发重绘,得到渲染耗时 2359 ms。
const times = 100000 let html = ‘‘ for(let i=0;i<times;i++) { html+= `<div>${i}</div>` } document.body.innerHTML += html const divs = document.querySelectorAll(‘div‘) Array.prototype.forEach.call(divs, (div, i) => { div.style.color = i % 2 ? ‘red‘ : ‘green‘; })
从两段测试代码中可以看出,重排渲染耗时明显高于重绘,同时两者的 Painting 事件耗时接近,也应证了重排会导致重绘。
如何高效操作 DOM
明白了 DOM 操作耗时之处后,要提升性能就变得很简单了,反其道而行之,减少这些操作即可。
在循环外操作元素
比如下面两段测试代码对比了读取 1000 次 JSON 对象以及访问 1000 次 body 元素的耗时差异,相差一个数量级。
const times = 10000; console.time(‘switch‘) for (let i = 0; i < times; i++) { document.body === 1 ? console.log(1) : void 0; } console.timeEnd(‘switch‘) // 1.873046875ms var body = JSON.stringify(document.body) console.time(‘batch‘) for (let i = 0; i < times; i++) { body === 1 ? console.log(1) : void 0; } console.timeEnd(‘batch‘) // 0.846923828125ms
当然即使在循环外也要尽量减少操作元素,因为不知道他人调用你的代码时是否出于循环中。
批量操作元素
比如说要创建 1 万个 div 元素,在循环中直接创建再添加到父元素上耗时会非常多。如果采用字符串拼接的形式,先将 1 万个 div 元素的 html 字符串拼接成一个完整字符串,然后赋值给 body 元素的 innerHTML 属性就可以明显减少耗时。
const times = 10000; console.time(‘createElement‘) for (let i = 0; i < times; i++) { const div = document.createElement(‘div‘) document.body.appendChild(div) } console.timeEnd(‘createElement‘)// 54.964111328125ms console.time(‘innerHTML‘) let html=‘‘ for (let i = 0; i < times; i++) { html+=‘<div></div>‘ } document.body.innerHTML += html // 31.919921875ms console.timeEnd(‘innerHTML‘)
虽然通过修改 innerHTML 来实现批量操作的方式效率很高,但它并不是万能的。比如要在此基础上实现事件监听就会略微麻烦,只能通过事件代理或者重新选取元素再进行单独绑定。批量操作除了用在创建元素外也可以用于修改元素属性样式,比如下面的例子。
创建 2 万个 div 元素,以单节点树结构进行排布,每个元素有一个对应的序号作为文本内容。现在通过 style 属性对第 1 个 div 元素进行 2 万次样式调整。下面是直接操作 style 属性的代码:
const times = 20000; let html = ‘‘ for (let i = 0; i < times; i++) { html = `<div>${i}${html}</div>` } document.body.innerHTML += html const div = document.querySelector(‘div‘) for (let i = 0; i < times; i++) { div.style.fontSize = (i % 12) + 12 + ‘px‘ div.style.color = i % 2 ? ‘red‘ : ‘green‘ div.style.margin = (i % 12) + 12 + ‘px‘ }
如果将需要修改的样式属性放入 JavaScript 数组,然后对这些修改进行 reduce 操作,得到最终需要的样式之后再设置元素属性,那么性能会提升很多。代码如下:
const times = 20000; let html = ‘‘ for (let i = 0; i < times; i++) { html = `<div>${i}${html}</div>` } document.body.innerHTML += html let queue = [] // 创建缓存样式的数组 let microTask // 执行修改样式的微任务 const st = () => { const div = document.querySelector(‘div‘) // 合并样式 const style = queue.reduce((acc, cur) => ({...acc, ...cur}), {}) for(let prop in style) { div.style[prop] = style[prop] } queue = [] microTask = null } const setStyle = (style) => { queue.push(style) // 创建微任务 if(!microTask) microTask = Promise.resolve().then(st) } for (let i = 0; i < times; i++) { const style = { fontSize: (i % 12) + 12 + ‘px‘, color: i % 2 ? ‘red‘ : ‘green‘, margin: (i % 12) + 12 + ‘px‘ } setStyle(style) }
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
reduce() 可以作为一个高阶函数,用于函数的 compose。
注意: reduce() 对于空数组是不会执行回调函数的。
从下面的耗时占比图可以看到,紫色 Rendering 事件耗时有所减少。
virtualDOM 之所以号称高性能,其实现原理就与此类似。
缓存元素集合
比如将通过选择器函数获取到的 DOM 元素赋值给变量,之后通过变量操作而不是再次使用选择器函数来获取。
下面举例说明,假设我们现在要将上面代码所创建的 1 万个 div 元素的文本内容进行修改。每次重复使用获取选择器函数来获取元素,代码以及时间消耗如下所示。
for (let i = 0; i < document.querySelectorAll(‘div‘).length; i++) { document.querySelectorAll(`div`)[i].innerText = i }
如果能够将元素集合赋值给 JavaScript 变量,每次通过变量去修改元素,那么性能将会得到不小的提升。
const divs = document.querySelectorAll(‘div‘) for (let i = 0; i < divs.length; i++) { divs[i].innerText = i }
对比两者耗时占比图可以看到,两者的渲染时间较为接近。但缓存元素的方式在黄色的 Scripting 耗时上具有明显优势
除了这些方法之外,还有一些原则也可能帮助我们提升渲染性能,比如:
希望你首先能理解原因,然后记住这些方法和原则,编写出高性能代码。
标签:计数器 终端 形式 模式 服务器 推荐 遇到 个数 拼接
原文地址:https://www.cnblogs.com/yingzi1028/p/13268550.html