Edge要兼容 Chrome 扩展

随着微软对 Edge 浏览器将采用 Chromium 内核的消息的确认,越来越多的人开始关注并议论此事。在 Reddit 上的相关话题下,有人写道:此举并不会有助于 Edge 的市场份额。现在没有用 Edge 的人,并不会因为一个 Chrome 克隆版,而去使用它吧?我们需要的是更多有竞争力的浏览器。Chrome 正在迅速成为新的 IE6 ,而且似乎所有人还在为它鼓掌欢呼。Edge 最大的问题是它和 Win 10 系统绑定。微软现在强调会将 Edge Chrome 放入商店,以便可以独立于操作系统进行更新,但这还不够 - edgeHTML 也需要在操作系统之外进行更新。或许如果 Edge 附带了自己的渲染引擎副本,它也可以跟上发展。微软最近非常看好 PWA 。我想知道这是否与他们担心 PWA 不会与 Edge 合作有关?他们之前似乎有打算通过开源 Chakra 来与 node.js /electron 应用竞争,但没有带来好的结果。也许他们担心 edgeHTML 和 PWA 会发生同样的事情?这或许也是微软不相信 UWP 及他们自己的应用商店的一种表现。这对 UWP WebView 控件意味着什么?它目前使用 edgeHTML 进行渲染。PWA 将如何运作?他们还会在 Windows 中使用嵌入式 edgeHTML 引擎吗?扩展怎么样?该评论发布后,被迅速顶至“最佳回复”,许多用户表示有相同的顾虑。该评论也引起了 Edge 开发团队的注意,Edge 项目经理 kylealden 出面进行了回复:现有的 UWP 应用(包括商店中的 PWA )将继续使用 EdgeHTML/Chakra 。我们没有打算做两个使用不同引擎的浏览器。我们倾向于提供一个基于新的渲染引擎的 WebView 以供用户使用。除了从微软商店下载安装外,我们还希望支持直接从浏览器安装 PWAs (与 Chrome 类似)。我们尚未准备好所有的细节,但 PWA 表现得像本地应用仍然是一个重要原则,所以我们将寻找合适的系统集成来实现这一目标。 ...

December 17, 2018 · 1 min · jiezi

人工智能涉足清洁行业,将带来哪些机遇和挑战?

随着人工智能技术的发展,越来越多的行业都在向科技靠拢,AII IN AI将是更多公司的发展战略。去年11月,“第九届中国清洁环卫论坛”在京召开。清洁领域的专业人士和众多企业家都在积极探讨如何改善环境质量,如何加速企业转型升级,如何加速新商业模式在传统产业中的推广和应用。在这些新契机下,将会诞生更多的商业机会。Leatu-Robot据清研智库发布的《2016-2017年度商业清洁行业发展趋势研究报告》数据显示,预计到2021年,大清洁市场规模将达到1,505亿元,其中清洁服务约占67.6%;小清洁市场规模将达到488.2亿元,未来五年复合增长率有望达到9.6%。这无疑是给保洁公司和清洁设备供应商带来了利好消息。目前清洁行业市场正在逐步规范,管理体系日趋完善,对外交流频繁,理念及技术迅速提高。传统的清洁行业想要获得突破性发展,就要从人员服务品质上下功夫,进而逐步实现无人化服务,实现绿色创新发展。未来的竞争是客户服务体验的竞争。前几天有个客户向我抱怨,“在过去的20多年里,清洁设备一直没有质的改变,唯一变的就是国内品牌更多了,进口设备更便宜了”。听了他的话,我很欣慰,现在保洁公司正在期待像我们一样的机器人研发公司,为他们提供代替人工和节省人工的机器人产品,真正的解放劳动力,实现降本增效的目的。随着人工智能和大数据技术逐步渗透到各个行业,未来一定会淘汰更多的重复性工作人员,但同时也会增加更多具有技术含量的蓝领岗位。未来清洁行业的发展离不开人工智能的参与。现在人工智能技术还处在起步阶段,各科技公司的机器人产品研发成本还很高,但随着技术逐步成熟和普及,将会给清洁行业带来翻天覆地的变化。不断为客户提供高品质的专业服务是我们的愿景,诚信经营是核心。希望不久的将来能够看到越来越多的专业服务机器人走上工作岗位,越来越多的企业敢于创新,拥抱变革,共同促进科技发展,社会进步。

December 14, 2018 · 1 min · jiezi

峰采 2018-12-09

开篇原创的文件实在没有时间写了。所以这个是一个文章,链接推荐贴。有点像阮老师的weekly。但是暂时不会是每周都有的。毕竟不是专职做这个的。内容嘛,没有限制。但多数是技术相关的。正文https://www.linuxjournal.com/…Chrome OS稳定版可以直接运行Linux Apps了。Windows 已经可以 apt-get了。Chrome OS现在也可以了。商业巨头在帮桌面Linux续命,而Ubuntu已经将重心转向了cloud。https://wetransfer.com/方便的传送大型文件https://www.cacher.io/方便快捷的代码短片记录。还是gist好?
https://www.bloomberg.com/amp…IBM收购红帽。$190 per share啊,要是我我也卖了。已是旧闻。但前几天有rumor说Google要截胡。应该不会吧。不管是谁买,一件事是可以肯定的:open source is eating the worldhttps://github.com/ncw/rclone类似Dropbox的开源文件同步软件。好处是对接各种云存储,包括国内的青云。做图床或者私有备份都可以。https://blog.cloudflare.com/c…技术就是用来被颠覆的。当容器,kubernetes在颠覆云计算的时候,cloudflare已经在颠覆容器了。这东西有个名字叫Worker。再过几年,也许大家就不再用docker, k8s说事了。。。。到时候我吃什么喝什么呢?https://www.slidescarnival.co…免费,好看的slides模板。支持Google Slides和Powerpointhttps://www.gatesnotes.com/Ab…比尔盖茨喜欢看《硅谷》。认为其中很多内容很真实。《硅谷》确实NB,我最喜欢的美剧之一。码农必刷。https://2018.stateofjs.com/co…2018 Javascript 报告https://nginxconfig.io流行软件里面有必Nginx的配置更多的吗?这个在线工具能减轻你配置Nginx的痛点。http://benhoad.net/hooli-sign…《硅谷》fans才能get到。有好事者创造了Hooli box signature edition。这样一本正经的幽默实在是无敌结束完成之后发现有图片会好很多。这次算了吧。

December 9, 2018 · 1 min · jiezi

身为前端开发工程师,你需要了解的搜索引擎优化SEO.

网站url网站创建具有良好描述性、规范、简单的url,有利于用户更方便的记忆和判断网页的内容,也有利于搜索引擎更有效的抓取您的网站。网站设计之初,就应该有合理的url规划。处理方式:1.在系统中只使用正常形式url,不让用户接触到非正常形式的url。2.不把session id、统计代码等不必要的内容放在url中。3.不同形式的url,301永久跳转到正常形式。4.防止用户输错而启用的备用域名,301永久跳转到主域名。5.使用robots.txt禁止Baiduspider抓取您不想向用户展现的形式。title信息 网页的title用于告诉用户和搜索引擎这个网页的主要内容是什么,而且当用户在百度网页搜索中搜索到你的网页时,title会作为最重要的内容显示在摘要中。搜索引擎在判断一个网页内容权重时,title是主要参考信息之一。描述建议:1.首页:网站名称 或者 网站名称_提供服务介绍or产品介绍 。2.频道页:频道名称_网站名称。3.文章页:文章title_频道名称_网站名称。需要注意:1.标题要主题明确,包含这个网页中最重要的内容。2.简明精练,不罗列与网页内容不相关的信息。3.用户浏览通常是从左到右的,重要的内容应该放到title的靠前的位置。4.使用用户所熟知的语言描述。如果你有中、英文两种网站名称,尽量使用用户熟知的那一种做为标题描述。 meta信息 meta description是meta标签的一部分,位于html代码的<head>区。meta description是对网页内容的精练概括。如果description描述与网页内容相符,百度会把description当做摘要的选择目标之 一,一个好的description会帮助用户更方便的从搜索结果中判断你的网页内容是否和需求相符。meta description不是权值计算的参考因素,这个标签存在与否不影响网页权值,只会用做搜索结果摘要的一个选择目标。推荐做法:1.网站首页、频道页、产品参数页等没有大段文字可以用做摘要的网页最适合使用description。2.为每个网页创建不同的description,避免所有网页都使用同样的描述。3.长度合理,不过长不过短。 图片alt 建议为图片加alt说明。因为这样做可以在网速较慢图片不能显示时让用户明白图片要传达的信息,也能让搜索引擎了解图片的内容。同理,使用图片做导航时,也可以使用alt注释,用alt告诉搜索引擎所指向的网页内容是什么。flash信息 Baiduspider只能读懂文本内容,flash、图片等非文本内容暂时不能处理,放置在flash、图片中的文字,百度无法识别。所以如果一定要使用flash,建议给object标签添加注释信息。这些信息会被看作是对Flash的描述信息。让搜索引擎更好的了解您flash的内容。frame框架 不建议使用frame和iframe框架结构,通过iframe显示的内容可能会被百度丢弃。收藏吧!

October 26, 2018 · 1 min · jiezi

一次排查页面重复请求的经历

前段时间重构一个页面,页面中存在通过第三方JavaScript代码插入的动态广告(正常的产品需求),上线后发现第三方的广告资源存在重复请求的问题。由于控制广告插入的JavaScript代码由第三方提供,我们只负责按照他们要求的方式引入即可,所以对JavaScript代码内容并不了解,在这种情况下开始了艰难的排查过程。虽困难重重,但最终还是找到了原因,在此过程中有些收获,现将排查过程抽象如下:注:以下过程和截图皆在Chrome浏览器中进行。一、代码<div id=“container”> <iframe src="/iframe-1" frameborder=“0”></iframe> <iframe src="/iframe-2" frameborder=“0”></iframe> <iframe src="/iframe-3" frameborder=“0”></iframe></div><script> document.getElementById(‘container’).innerHTML += ‘<p>上面是iframe</p>’;</script>代码大意:页面上先渲染3个iframe(目前页面插入广告仍然以iframe作为主要实现形式),然后在最后一个iframe后面追加一个p元素二、现象1.页面:渲染正常2.Network:存在重复的异常请求(Status是canceled)三、排查过程1.重复请求从何而来?既然是解决重复请求的问题,那么重复请求从何而来是我们要解决的第一问题。由于请求是从第三方的JavaScript代码中发出的,去读第三方压缩后的JavaScript代码更像无头苍蝇。整个过程就像在围城之外徘徊,心急如焚。后来静下心来发现Chrome的devtools中一个很关键的排查助力神器:Network下的Initiator此列是什么意思呢?通俗地说就是触发请求的位置。通过对比发现,同一个重复的请求发起的位置并不相同,以/iframe-1为例:点击第一个请求的Initiator,跳转的位置(标黄位置):点击第二个请求的Initiator,跳转的位置(标黄位置):通过观察可以发现,第一个/iframe-1请求是由于正常渲染iframe元素自动触发的,第二个/iframe-1请求是在执行JavaScript代码(作用拼接DOM节点)时触发的,然而对于触发第二个/iframe-1请求的那行JavaScript代码,其真实意图仅仅是拼接一个p元素而已,并不期望其他额外的事情(比如触发新的请求)发生。另外,对于/iframe-2和/iframe-3的第二次请求的触发点都是那段拼接DOM节点的JavaScript代码,至此,产生问题的罪魁祸首已经浮出水面,接下来我们分析下产生重复请求的原因。2.为什么会重复请求?产生重复请求的JavaScript代码document.getElementById(‘container’).innerHTML += ‘<p>上面是iframe</p>’;翻译成:document.getElementById(‘container’).innerHTML = document.getElementById(‘container’).innerHTML + ‘<p>上面是iframe</p>’;意思就明了多了:先获取id为container的div元素的所有内部HTML,将其拼接p元素后,再赋值给container的innerHTML。这个过程会导致iframe元素的重新渲染,也就会引发iframe对应的请求重新触发。所以,同一个请求会触发两次的原因:页面加载时渲染iframe元素会触发第一次请求,执行JavaScript代码导致iframe重新渲染触发第二次请求。找到了问题的原因,解决问题的办法也就水到渠成了,将document.getElementById(‘container’).innerHTML += ‘<p>上面是iframe</p>’;改为:var div = document.createElement(‘div’);var text = document.createTextNode(‘广告’);div.appendChild(text);document.getElementById(‘container’).appendChild(div);问题解决了,不过,还有一个疑问:为什么渲染iframe产生的第一个请求的状态是canceled?3.为什么重复的请求的Status是canceled?首先Status是canceled代表什么意思呢?从其字面意思理解,代表此请求被取消了,即此请求在发给服务器端之前就被浏览器取消了,也就是说此请求根本就没有从浏览器发出去,更不可能到达服务器,所以状态是canceled而不是HTTP状态码也就不难理解了。那第一次的请求为什么会被浏览器取消呢?用关键词“chrome cancel request”谷歌了一下,在stack overflow上找到了一个比较全面的解答,截图如下:其中红色标注即为我们要寻找的答案。根据截图大概梳理一下,Chrome浏览器会取消请求的几种场景:触发请求的DOM元素被删除了(比如img元素还没有加载完就被删除了)做了一些不必要的数据加载(比如开始加载iframe后改变其src或重写其内容)大量的请求指向同一个服务器,并且前面请求的网络问题表明后续的请求也走不通(DNS查询错误,前面的请求报400)至此,整个过程中的疑问点一一解开了。四、总结现在再回顾此bug,产生的原因并不高深,但整个排查过程确实值得总结。小结一下:1.对于第三方库报错,切莫妄图通过通读并熟悉整个库后解决问题,通读代码只会浪费解决问题的时间,弄明白调用关系才是王道2.Chrome开发者工具中的Network > Initiator代表请求是从哪里触发的,对于定位请求非常有用,尤其是对于一些第三方库中发出的请求3.请求状态为canceled,表示请求被浏览器取消了,并没有从浏览器发出去,更不可能进到服务器4.Chrome浏览器取消请求的几种情景,见上图5.element.innerHTML += HTMLStr 表示将原有的子节点和新的节点拼接后再重新赋值,会导致节点元素重新渲染,节点内容中含有iframe时慎用

October 23, 2018 · 1 min · jiezi

一篇文章掌握nightwatch自动化测试

nightwatch.js是一个web-ui自动化测试框架,被vue-cli深度整合进来。如果一个项目是基于vue-cli搭建的,基本可以做到开箱即用。但是我们不可能一直都使用vue-cli。因为它很多时候不能够满足我们的定制化需求。我们很多时候会对构建框架进行定制,或者完全重新搭建。这个时候整合进来nightwatch就会很困难。这篇文章就来带着大家入门搭建这么一个测试框架。所需环境首先在项目里面安装nightwatch,切换到项目目录npm intall nightwatch -D安装这个还不够,还需要安装selenium-server,同样可以使用npm安装npm install selenium-server -Dselenium-server是基于Java开发的,作用是用来连接浏览器的。所以安装selenium-server之前需要安装java。如何安装Java?除此之外还需要安装一个浏览器驱动器,一般我们使用chrome做测试npm install chromedriver –chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedrive -D直接安装chromedriver会提示安装不成功,需要手动指定镜像地址至此需要下载的部分已经下载完成。项目配置nightwatch默认使用项目根目录的nightwatch.conf.js作为配置文件。官网也有另外一种配置文件的格式就是nightwatch.json。这里我们使用js的配置方式,因为格式更灵活。下面这个就是一个简单的配置文件。module.exports = { ‘src_folders’: [ ’e2e/case’ ], ‘output_folder’: ‘reports’, ‘custom_commands_path’: ‘’, ‘custom_assertions_path’: ‘’, ‘page_objects_path’: ‘’, ‘globals_path’: require(’./e2e/config/global.config’).path, ‘selenium’: { ‘start_process’: true, ‘server_path’: require(‘selenium-server’).path, ’log_path’: ‘’, ‘host’: ‘127.0.0.1’, ‘port’: 4444, ‘cli_args’: { ‘webdriver.chrome.driver’: require(‘chromedriver’).path } }, ’test_settings’: { ‘default’: { ’launch_url’: ‘http://localhost’, ‘selenium_port’: 4444, ‘selenium_host’: ’localhost’, ‘silent’: true, ‘screenshots’: { ’enabled’: false, ‘path’: ’’ }, ‘desiredCapabilities’: { ‘browserName’: ‘chrome’, ‘marionette’: true } }, ‘chrome’: { ‘desiredCapabilities’: { ‘browserName’: ‘chrome’ } }, ’edge’: { ‘desiredCapabilities’: { ‘browserName’: ‘MicrosoftEdge’ } } }}src_folders:表示的就是case所在的文件夹output_folder:代表的是报告输出的文件夹selenium下面的server_path:代表的是selenium-server的安装路径selenium下面的start_process:代表的是是否自动启动selenium——server,入股设为false,不会自动启动server。“cli_args” : { “webdriver.chrome.driver” : “”, “webdriver.gecko.driver” : “”, “webdriver.edge.driver” : "" }cli_args下面的driver表示几个driver的安装路径,分别安装成功就可以了test_settings是传给nightwatch实例的数据,这里面可以配置多个环境,default是必须有的,其他环境可以自行配制。nightwatch –env default然后我们在项目里运行以上命令。在windows发现报错了,运行不起来的。我们需要在package.json下面配置一下"scripts": { “e2e”: “nightwatch –env default”, },这就可以正常运行了。如果需要看到真实的效果,我们在case文件夹下面创建一些case就可以了比如:module.exports = { ‘Test login’: function (browser) { browser .windowMaximize() .url(‘https://trans.qa.17u.cn/saas') .waitForElementVisible(’.login’, 3000) .assert.urlContains(’/saas/login’) } } ...

October 17, 2018 · 1 min · jiezi

「译」代码优化策略 — Idle Until Urgent

Idle Until Urgent(闲置直到紧急)译者注:大家耳熟能详的优化策略已经谈论了好多年了,用 Chrome 性能分析工具发现瓶颈并针对性优化的文章网络上也有不少,但是从运行时调度策略来思考优化方式的却凤毛麟角,正如我们之前只知道使用 setTimeout 来进行 throttling 和 debounce。因此在偶然看到这篇文章时,我有一种__豁然开朗__的感觉:原来我们还可以在这么细致的粒度上进行调度。原文:https://philipwalton.com/articles/idle-until-urgent/几周前,我正着手查看我网站的一些性能指标。具体来说,我想看看我在我们最新的性能标准,即首次输入延迟 (FID)上的表现。由于我的网站只是一个博客(并没有运行很多 JavaScript ),所以我希望我能看到一个相当不错的结果。小于 100 毫秒的输入延迟通常被用户视为即时响应,因此我们建议的性能目标(以及我希望在我的分析中看到的数字)是:对于 99% 的页面加载来说,FID <100ms。令我惊讶的是,我的网站在第 99 百分位数下的 FID 为 254 毫秒。虽然那并不可怕,但我的完美主义性格却令我无法松懈。嗯,我必须解决它!总而言之,在不删除我网站的任何功能的情况下,我需要能够在第 99 百分位数下将我的FID控制在 100 毫秒以下。但我确信,你们这些读者更感兴趣的是以下信息:我是_如何_诊断问题的。我用了_什么_具体的策略和技术来解决问题。对于上面的第二点,当我试图解决我的问题时,我偶然发现了一个让我想分享的,非常有趣的性能策略(这就是我写这篇文章的主要原因)。我将这个策略称之为:idle-until-urgent(闲置直到紧急) 。我的性能问题首次输入延迟(FID)是一个度量标准,用于衡量用户首次与您的网站进行交互的时间(对于像我这样的博客来说,最有可能的情况是点击链接)以及浏览器能够响应该互动的时间(对于我博客的点击交互来说,就是请求加载下一页)。这里可能存在延迟的原因是浏览器的主线程正在忙于做其他事情(通常是执行 JavaScript 代码)。因此,要诊断出高于预期的 FID,您首先应当做的是在页面加载时启用站点的性能跟踪(同时启用 CPU 和网络限制),然后在主线程上查找需要执行很长时间的各个任务。一旦确定了这些长任务,你就可以尝试将它们分解为更小的任务。以下是我在对网站进行性能跟踪时的发现:一份加载我网站时的 JavaScript 性能跟踪(启用网络/ CPU限制)。请注意,在 main 脚本 bundle 执行时,它作为单个任务需要 233 毫秒才能运行完成。执行我网站的 main bundle 需要 233 毫秒。这些代码中的一些是 webpack boilerplate 和 babel polyfill,但大多数代码来自我脚本中的 main() 入口函数,它本身需要 183 毫秒才能完成:执行我的站点的main()入口函数需要 183 毫秒。然而我并没有在我的 main() 函数中做什么奇怪的事情。我在函数中只是初始化我的 UI 组件,然后运行分析:const main = () => { drawer.init(); contentLoader.init(); breakpoints.init(); alerts.init(); analytics.init();};那么到底是什么花了这么长时间在运行?好吧,如果你看一下这个火焰图的尾部,你不会看到有任何函数在执行时明显地占据了大部分时间。大多数单个函数会在不到 1 毫秒的时间内运行,但是当你将它们全部添加起来之后,在单个同步调用堆栈中运行它们就需要花费超过 100 毫秒。这就是杀千刀的 JavaScript。由于问题是所有这些功能都作为单个任务的一部分在运行,因此浏览器必须等到此任务完成才能够响应用户的交互。很明显,解决方案是将这些代码分解为多个任务。但这说起来容易,做起来难。乍一看,似乎显而易见的解决方案是给 main() 函数中的每个组件排个优先级(它们实际上已经按优先级顺序排列),最快初始化最高优先级的组件,然后将其他组件的初始化推迟到后续的任务去做。虽然这个方法可能对某些人有所帮助,但它并不是每个人都可以实施的通用解决方案,也不能很好地扩展到一个非常大的网站中。原因如下:推迟 UI 组件初始化仅在组件尚未渲染时才有用。如果它已经被渲染过了,那么延迟这个组件的初始化运行,则会带来在用户交互时组件并未准备好的风险。在许多情况下,所有 UI 组件要么同等重要,要么彼此依赖,因此它们都需要同时进行初始化。有时单个组件需要足够长的时间来初始化,此时即使它们只在自己的任务中运行,它们也会阻塞主线程。实际情况是,在自己的任务中初始化每个组件通常是不够高效的,并且往往是不可能的。我们通常需要把任务分解到每个被初始化的组件中。贪婪的组件一个真正需要将其初始化代码分解的组件的完美示例可以通过将此性能跟踪结果进一步缩放观察看到。在 main() 函数的中间,你会看到我的一个组件使用 Intl.DateTimeFormat API:创建 Intl.DateTimeFormat 实例花了 13.47ms!创建此对象需要 13.47 毫秒!问题在于, Intl.DateTimeFormat 实例虽然在组件的构造函数中被创建了,但实际上并没有被引用,直到其他组件将其用于格式化日期为止。但是,此组件不知道它什么时候会被引用,因此它只能谨慎行事,立即实例化 Int.DateTimeFormat 对象。但这真的是正确的代码求值策略吗?如果不是,那正确的应该是什么?代码求值策略在为可能执行代价高昂的代码选择求值策略时 ,大多数开发人员会选择以下其中一项:及早求值(Eager evaluation):您可以立即运行代价高昂的代码。惰性求值(Lazy evaluation):你等到程序的另一部分需要这段代价高昂代码的结果时,你才运行它。这也许是两种最受欢迎的求值策略,但在重构完我的网站后,我现在认为这些可能是你最糟糕的选择。及早求值的缺点我网站上的性能问题很好地说明了及早求值的一个缺点,即如果用户在代码求值时尝试与您的页面进行交互,浏览器必须等到代码完成求值才能做出响应。如果您的页面看起来已准备好响应用户输入,但实际上却无法响应,在这种情况下,这尤其成问题。用户会认为您的页面缓慢甚至完全是坏的。你预先求值的代码越多,您的页面达到可以交互所需的时间就越长。惰性求值的缺点如果立即运行所有代码是不好的,那么最明显的解决方案就是等到实际需要它的时候再运行。这样就不会不必要地运行代码,特别是在用户实际上从未需要它的情况下。当然,等到用户需要该代码的结果时再运行代码的问题在于,用户输入肯定会被你那些代价高昂的代码给堵塞住。对于某些事情(比如从网络加载其他内容),将其推迟到用户请求时再执行是有意义的。但是对于您正在运行的大多数代码(例如从 localStorage 读取数据,处理大型数据集等),您肯定希望在需要它的用户交互开始之前就能开始执行。其他选择你也可以在及早和惰性求值之间选取一种其它求值策略,我不确定以下两种策略是否有官方名称,但我会称之为延迟求值和空闲求值:延迟求值(Deferred evaluation):使用类似于 setTimeout 之类的方法将代码安排在一个未来的任务里执行__空闲求值(Idle evaluation)__:一种延迟求值,您可以使用像 requestIdleCallback 这样的API来安排代码执行。这两个选项通常都比及早或惰性求值更好,因为它们不太可能导致阻止输入的单个长任务发生。这是因为,虽然浏览器无法中断任何一个任务以响应用户输入时(这样做将极有可能让页面崩溃),但它可以在计划任务的队列之间运行任务,大多数浏览器会将用户输入引发的任务这么安排。这称为输入优先级 。换句话说:如果能够确保所有代码都运行在简短,不同的任务中(最好少于 50 毫秒 ),您的代码将永远不会堵塞用户输入。重要! 虽然浏览器可以在排队任务之前执行输入的回调,但它们无法在排队的微任务之前运行输入回调。由于 promises 和 async 函数会作为微任务运行,所以将同步代码转换为基于 promise 的代码并不会避免它堵塞用户输入!如果您不熟悉任务和微任务之间的区别,我强烈建议您观看我的同事 Jake 关于事件循环的精彩演讲 。鉴于我刚才所说,我可以重构我的 main() 函数,使用 setTimeout() 和 requestIdleCallback() 将我的初始化代码分解为独立的任务:const main = () => { setTimeout(() => drawer.init(), 0); setTimeout(() => contentLoader.init(), 0); setTimeout(() => breakpoints.init(), 0); setTimeout(() => alerts.init(), 0); requestIdleCallback(() => analytics.init());};main();;然而,虽然这比以前好了一点(许多小任务 vs 一项长任务),但正如我上面解释的那样,它可能仍然不够好。例如,如果我推迟我 UI 组件(特别是 contentLoader 和 drawer)的初始化,它们将不太可能堵塞用户输入,但是当用户尝试与它们交互时,它们也存在未准备好的风险!虽然使用 requestIdleCallback() 将我的 analytics 推迟可能是一个好主意,但在下一个空闲时段之前我关心的任何交互都将被遗漏。如果在用户离开页面之前没有空闲时段,这些回调代码可能永远不会运行!因此,如果所有的求值策略都有缺点,你应该选择哪一个呢?Idle Until Urgent (闲置直到紧急)在花了很多时间思考这个问题之后,我意识到我真正想要的求值策略是让我的代码在最初时被推迟到空闲时段,但是在需要时能够立即运行。换句话说: idle-until-urgent 。idle-until-urgent 策略避免了我在上一节中描述的大多数缺点。在最坏的情况下,它具有与延迟求值完全相同的性能特征,并且在最好的情况下它根本不堵塞交互性,因为代码执行发生在空闲期间。我还应当提到的一点是,这种策略既适用于单个任务(闲时计算值),也适用于多个任务(可以在空闲时运行的一个有序任务队列)。我将首先解释单任务(空闲值)形式,因为它更容易理解。空闲值在上面,我向大家展示了初始化 Int.DateTimeFormat 对象可能代价非常昂贵,因此如果不是立即需要这个实例的话,最好在空闲期间初始化它。当然,一旦当它被需要时,你就想让它存在,所以这是一个 idle-until-urgent 求值策略的完美候选对象。考虑以下我们要重构以使用此新策略的简化组件示例:class MyComponent { constructor() { addEventListener(‘click’, () => this.handleUserClick()); this.formatter = new Intl.DateTimeFormat(’en-US’, { timeZone: ‘America/Los_Angeles’, }); } handleUserClick() { console.log(this.formatter.format(new Date())); }}上面的 MyComponent 实例在它的构造函数中做了两件事:为用户交互添加事件侦听器。创建 Intl.DateTimeFormat 对象。该组件完美地说明了为什么您经常需要在单个组件内部拆分任务(而不仅仅是在组件层面上拆分)。在这种情况下,事件监听器的立即运行非常重要,但在事件处理程序需要用到之前,是否创建了 Intl.DateTimeFormat 实例并不重要。当然我们不想在事件处理程序中创建 Intl.DateTimeFormat 对象,因为它的龟速会推迟该事件的运行。所以,这就是我们更新此代码以使用 idle-until-urgent 策略的方法。注意,我正在使用 IdleValue 辅助类,我将在下面解释它:import {IdleValue} from ‘./path/to/IdleValue.mjs’;class MyComponent { constructor() { addEventListener(‘click’, () => this.handleUserClick()); this.formatter = new IdleValue(() => { return new Intl.DateTimeFormat(’en-US’, { timeZone: ‘America/Los_Angeles’, }); }); } handleUserClick() { console.log(this.formatter.getValue().format(new Date())); }}正如你所见,这个代码与以前的版本看起来没有太多不同,但是与将 this.formatter 分配给一个新的 Intl.DateTimeFormat 对象相反,我将 this.formatter 分配给了一个 IdleValue 对象,在对象中我传递了一个初始化功能。此 IdleValue 类起作用的方式是,它会调度在会下一个空闲期间运行的初始化函数。如果空闲阶段发生在 IdleValue 被引用实例之前,则不会发生阻塞,并且可以在请求时立即拿到该返回值。但另一方面,如果在下一个空闲周期_之前_引用该值,则安排好的空闲回调会被取消,并且初始化函数会被立即调用。下面是如何实现 IdleValue 类的要点(注意:我还发布了这段代码作为 idlize 包 的一部分,其中包含了本文中显示的所有 helper 类):export class IdleValue { constructor(init) { this._init = init; this._value; this._idleHandle = requestIdleCallback(() => { this._value = this._init(); }); } getValue() { if (this._value === undefined) { cancelIdleCallback(this._idleHandle); this._value = this._init(); } return this._value; } // …}}虽然在上面的示例中引入 IdleValue 类并不需要很多更改,但它在技术上改变了公共API( this.formatter 与 this.formatter.getValue() )。如果您处于想要使用 IdleValue 类但无法更改公共 API 的情况,则可以将 IdleValue 类与 ES2015 getter 一起使用:class MyComponent { constructor() { addEventListener(‘click’, () => this.handleUserClick()); this._formatter = new IdleValue(() => { return new Intl.DateTimeFormat(’en-US’, { timeZone: ‘America/Los_Angeles’, }); }); } get formatter() { return this.formatter.getValue(); } // …}}或者,如果你不介意一点抽象,你可以使用 defineIdleProperty() helper 类(它在底层使用了 Object.defineProperty() ):import {defineIdleProperty} from ‘./path/to/defineIdleProperty.mjs’;class MyComponent { constructor() { addEventListener(‘click’, () => this.handleUserClick()); defineIdleProperty(this, ‘formatter’, () => { return new Intl.DateTimeFormat(’en-US’, { timeZone: ‘America/Los_Angeles’, }); }); } // …}}对于计算成本可能很高的单个属性值,实际上没有理由不使用此策略,尤其是你可以在不更改API的情况下使用它!虽然这个例子使用了 Intl.DateTimeFormat 对象,但它也可能是下列任一项操作的好候选方案:处理大量值的集合。从 localStorage(或 cookie )获取值。运行 getComputedStyle() , getBoundingClientRect() 或任何其他可能需要在主线程上重新计算样式或布局的 API。空闲任务队列上述技术适用于其值可以使用单个函数计算的独立属性,但在某些情况下,您的逻辑可能不适合用单个函数表达,或者,即使技术上可行,您仍然希望将它分解为几个更小的函数,因为不这样做的话,你可能会长时间阻塞主线程。在这种情况下,您真正需要的是一个队列,您可以在其中安排多个任务(函数),在浏览器空闲时运行。队列将在可能的情况下运行任务,并且当需要回到浏览器时(例如,如果用户正在进行交互),它将暂停执行任务。为了解决这个问题,我构建了一个 IdleQueue 类,您可以像这样使用它:import {IdleQueue} from ‘./path/to/IdleQueue.mjs’;const queue = new IdleQueue();queue.pushTask(() => { // Some expensive function that can run idly…});queue.pushTask(() => { // Some other task that depends on the above // expensive function having already run…});注意:将同步 JavaScript 代码分解为可作为任务队列的一部分异步运行的单独任务与代码分割不同,后者是将大型 JavaScript bundle 分解为较小的文件(这对于提高性能也很重要)。与上面显示的空闲初始化属性策略一样,空闲任务队列也可以在需要立即执行结果的情况下立即运行(即“紧急”情况下)。同样,最后一点非常重要:有时不仅是因为你需要尽快计算某些东西,而是通常因为你要与同步的第三方 API 集成,所以为了兼容,你需要能够同步运行你的任务。在一个完美的世界中,所有 JavaScript API 都是非阻塞的,异步的,并且由可以随意返回主线程的小块代码组成。但在现实世界中,由于遗留代码库或与我们无法控制的第三方库的集成,我们通常别无选择,只能保持同步。正如我之前所说,这是 idle-until-urgent 模式的巨大优势之一。它可以轻松应用于大多数程序,而无需大规模重写架构。保证紧急情况我在上面提到过 requestIdleCallback() 并没有保证回调将会运行。在与开发人员讨论 requestIdleCallback() 时,这是我听到的他们不使用它的主要原因。在许多情况下,代码无法运行的可能性足以成为不使用代码的理由 - 为了使代码安全运行并保持代码同步(当然同时也会阻塞)。一个完美的例子就是分析代码。分析代码的问题是在很多情况下需要在页面卸载时运行(例如跟踪出站链接点击等),在这种情况下, requestIdleCallback() 根本无法作为一个选项,因为回调永远不会运行。由于分析库不知道他们的用户何时会在页面生命周期中调用他们的 API,故而他们也倾向于安全并同步运行所有代码(这很不幸,因为分析代码对于用户体验来说并不关键)。但是在 idle-until-urgent 模式下,有一个简单的解决方案。我们所要做的就是确保队列只要当页面处于可能很快卸载的状态,就会立即运行。如果您熟悉我在最近关于 Page Lifecycle API 的文章中给出的建议,您就会知道在页面被终止或丢弃之前,最后一个开发者可以依赖的可靠回调是 visibilitychange 事件(因为页面的 visibilityState 变为隐藏)。而且由于在隐藏状态下用户无法与页面进行交互,因此这是运行任何排队中的空闲任务的最佳时机。实际上,如果使用 IdleQueue 类,则我们可以使用传递给构造函数的简单配置项来启用此功能。const queue = new IdleQueue( { ensureTasksRun : true } );对于渲染等任务,无需确保在页面卸载之前运行任务,但对于保存用户状态和在会话结束时发送分析等任务,你可能希望将此选项设置为 true。注意:监听 visibilitychange 事件应该足以确保在卸载页面之前运行任务,但是由于 Safari 的漏洞,当用户关闭选项卡时, pagehide 和 visibilitychange 事件并不总是触发 ,你必须针对 Safari 实现一个应急方法。这个解决方法在 IdleQueue 类中已经为您实现 ,但如果您自己实现它,则必须对它有足够了解。警告! 不要为了在页面卸载之前运行队列而监听 unload 事件。unload 事件并不可靠,并且在某些情况下会有损性能。有关更多详细信息,请参阅我的 Page Lifecycle API 文章 。idle-until-urgent 的用例每当你需要运行可能代价高昂的代码时,你应该尝试将其分解为更小的任务。如果现在不需要立即使用该代码,但未来某些时候可能需要该代码,那么它就是一个完美的,可使用空闲直到紧急策略的用例 。在你自己的代码中,我建议做的第一件事就是查看你所有的构造函数,如果它们中的任何一个会运行可能很耗时的操作,那么重构它们以使用 IdleValue 对象来代替。对于其他的一些逻辑,如果这些逻辑对于直接用户交互是必要的,但并不一定是决定性的,那么请考虑将该逻辑添加到 IdleQueue 。不用担心,如果你需要立即运行该代码,你随时可以。特别适合这种技术的两个具体示例(并且与大部分网站相关)是持久化应用程序状态(例如,使用Redux之类)和网站分析。注意:这些用例想表明的意图是任务应该在空闲期间运行,当然,如果它们没有立即运行也没有问题。如果您需要处理高优先级的任务,这些任务旨在尽快运行(但仍然需要响应输入),那么 requestIdleCallback() 可能无法解决您的问题。幸运的是,我的一些同事提出了新的Web平台API( shouldYield() 和原生的 Scheduling API ),它们可能会对你有所帮助。持久化应用状态考虑这样一个 Redux 应用程序,它将应用程序状态存储在内存中,但也需要将其存储在持久存储(如 localStorage )中,以便下次用户访问页面时可以重新加载。在 localStorage 中存储状态的大多数 Redux 应用程序使用的 debounce 技术大致是这样的:let debounceTimeout;// Persist state changes to localStorage using a 1000ms debounce.store.subscribe(() => { // Clear pending writes since there are new changes to save. clearTimeout(debounceTimeout); // Schedule the save with a 1000ms timeout (debounce), // so frequent changes aren’t saved unnecessarily. debounceTimeout = setTimeout(() => { const jsonData = JSON.stringify(store.getState()); localStorage.setItem(‘redux-data’, jsonData); }, 1000);});虽然使用 debounce 技术肯定比什么都不用好,但它并不是一个完美的解决方案。问题是你无法保证当 debounced 函数运行时,它不会在对用户来说很关键的时间点阻塞主线程。在空闲时间安排 localStorage 写入会好得多。你可以将上述代码从 debounce 策略转换为 idle-until-urgent 策略,如下所示:const queue = new IdleQueue({ensureTasksRun: true});// Persist state changes when the browser is idle, and// only persist the most recent changes to avoid extra work.store.subscribe(() => { // Clear pending writes since there are new changes to save. queue.clearPendingTasks(); // Schedule the save to run when idle. queue.pushTask(() => { const jsonData = JSON.stringify(store.getState()); localStorage.setItem(‘redux-data’, jsonData); });});请注意,此策略肯定比使用 debounce 更好,因为即使用户离开页面,它也可以保证状态得到保存。而使用 debounce 的例子,写入可能会在这种情况下失败。网站分析_idle-until-urgent 的另一个完美用例是分析代码。下面是一个示例,说明如何使用 IdleQueue 类来安排发送分析数据,以确保即使用户关闭选项卡或在下一个空闲时段之前导航网页, 也会发送分析数据。const queue = new IdleQueue({ensureTasksRun: true});const signupBtn = document.getElementById(‘signup’);signupBtn.addEventListener(‘click’, () => { // Instead of sending the event immediately, add it to the idle queue. // The idle queue will ensure the event is sent even if the user // closes the tab or navigates away. queue.pushTask(() => { ga(‘send’, ’event’, { eventCategory: ‘Signup Button’, eventAction: ‘click’, }); });});除了确保紧急时执行之外,将此任务添加到空闲队列还可确保它不会阻止任何其他响应用户单击操作所需的代码。实际上,通常最好的方法是在闲暇时运行所有分析代码,包括初始化代码。对于像 analytics.js 这样的 API 已经成为队列的库 ,可以很容易地将这些命令添加到我们的 IdleQueue 实例中。例如,您可以将 默认analytics.js安装代码段 的最后一部分这样转换: 从:ga(‘create’, ‘UA-XXXXX-Y’, ‘auto’);ga(‘send’, ‘pageview’);转成这样:const queue = new IdleQueue({ensureTasksRun: true});queue.pushTask(() => ga(‘create’, ‘UA-XXXXX-Y’, ‘auto’));queue.pushTask(() => ga(‘send’, ‘pageview’));(你也可以给 ga() 函数创建一个能够自动把命令加入队列的 wrapper,这就是我所做的 )。requestIdleCallback 的浏览器支持在撰写本文时,只有 Chrome 和 Firefox 支持 requestIdleCallback() 。虽然真正的 polyfill 是不可能实现的(因为只有浏览器自己可以知道它何时空闲),但是很容易编写一个退回 setTimeout 的 fallback(本文中提到的所有 helper 类和方法都使用了这个 fallback )。即使在不原生支持 requestIdleCallback() 的浏览器中, 使用 setTimeout 的 fallback 肯定比不使用此策略更好,因为浏览器仍然可以在通过 setTimeout() 进行排队的任务之前进行输入优先级排序。这实际上提高了多少性能?在本文开头我提到我想出了这个策略,是因为我试图提高我的网站的 FID 值。我试图将 main bundle 加载后的立即运行的所有代码分割开,但我还需要确保我的网站继续使用只有同步 API 的某些第三方库(例如 analytics.js )。我在实现 idle-until-urgent 之前做的跟踪显示出,我有一个包含所有初始化代码的233ms 任务。在实现我在此描述的技术之后,你可以看到我有多个更短时间的任务。事实上,最长的一个现在只有37毫秒!我网站的 JavaScript 的性能跟踪显示了许多简短的任务。这里要强调的一个非常重要的一点是,它完成的工作和之前是一样的,只是现在分散在多个任务上并在闲置期间运行。因为没有任何一项任务超过 50 毫秒,所以没有一个任务影响我的交互时间(TTI),这对我的 lighthouse 得分很有帮助:我实施了 _idle-until-urget 之后的 Lighthouse 报告。_最后,由于所有这些工作的重点是提高我的FID,在将这些更改发布到生产环境并查看结果后,我很高兴发现我在第 99 百分位数下的 FID 值减少了67%!代码版本FID(p99)FID(p95)FID(p50)在 idle-until-urgent 之前254ms20ms3ms在 idle-until-urgent 之后285ms16ms3ms结论在一个完美的世界中,我们的网站永远不会在不必要的时刻阻塞主线程。我们都使用 Web worker 来完成非 UI 的工作,并且我们在浏览器中内置了 shouldYield() 和原生 Scheduling API 。但是在我们当前的世界中,我们的 Web 开发人员通常别无选择,只能在主线程上运行非 UI 代码,这会导致无响应。希望本文能够说服你切分长期运行的 JavaScript 任务。而且,由于 idle-until-urgent 可以将看起来同步的 API 变成实际上在空闲时段运行的代码,因此它是一个很好的解决方案,并适用于我们今天广泛使用的库。如果您喜欢这篇文章并认为其他人也应该阅读它,请在 Twitter 上分享 。文章可随意转载,但请保留此 原文链接。非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。 ...

September 30, 2018 · 4 min · jiezi