解决Moment.js性能问题的故事

Moment.js是用于解析和格式化日期的最受欢迎的JavaScript库之一。 WhereTo使用Node.js,因此对于他们来说,使用此库是完全自然的举动。 预计服务器使用Moment.js不会出现问题。 最后,从一开始,他们就在前端使用此库来显示日期,并对其工作感到满意。 但是,库在客户端上表现良好的事实并不意味着服务器也不会出现问题。



该材料(我们今天发布的翻译)专用于解决性能问题Moment.js的故事。

项目增长和生产力下降


最近,WhereTo系统返回的飞行记录数量增长了大约十倍。 然后,我们面临着非常严重的性能下降。 事实证明,渲染周期不到100毫秒,现在需要3秒钟以上才能显示大约5,000个搜索结果。 我们的团队已开始研究。 经过几次性能分析会话后,我们注意到有99%以上的时间都花在了一个名为createInZone函数中。


createInZone函数大约需要3.3秒才能完成。

继续对情况进行调查,我们发现此函数由Moment.js parseZone函数调用。 她为什么这么慢? 我们感觉Moment.js库是为常见用例设计的,因此,它将尝试以各种方式处理输入字符串。 也许您应该限制它? 阅读文档后,我们发现parseZone函数接受一个指定日期格式的可选参数:

 moment.parseZone(input, [format]) 

我们要做的第一件事是尝试使用parseZone函数,并将日期格式的信息传递给它,但是,正如性能测试所示,它并没有导致任何结果:

 $ node bench.js moment#parseZone x 22,999 ops/sec ±7.57% (68 runs sampled) moment#parseZone (with format) x 30,010 ops/sec ±8.09% (77 runs sampled) 

尽管现在parseZone函数的工作速度更快,但是对于我们的需求而言,这种速度显然还不够。

特定于项目的优化


我们使用Moment.js解析了从提供商的API(Travelport)中检索到的日期。 我们意识到它总是以相同的格式返回数据:

 "2019-12-03T14:05:00.000-07:00" 

知道了这一点,我们开始理解Moment.js的内部结构,以便(如我们希望的那样)编写一个效率更高的函数,产生相同的结果。

创建一个更快的替代parseZone


首先,我们需要弄清楚Moment.js对象的外观。 这很容易理解:

 > const m = moment() > console.log(m) Moment {  _isAMomentObject: true,  _i: '2019-12-03T14:05:00.000-07:00',  _f: 'YYYY-MM-DDTHH:mm:ss.SSSSZ',  _tzm: -420,  _isUTC: true,  _pf: { ...snip },  _locale: [object Locale],  _d: 2019-12-03T14:05:00.000Z,  _isValid: true,  _offset: -420 } 

下一步是不使用构造函数实例化Moment:

 export function parseTravelportTimestamp(input: string) {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  return m } 

现在看来,我们已经可以设置Moment实例的很多属性(我不详细介绍如何找到它,但是如果您查看Moment.js的源代码,您将会理解):

 const FAKE = moment() const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ' export function parseTravelportTimestamp(input: string) {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  const offset = 0 // TODO  const date = new Date(input.slice(0, 23))  m._isAMomentObject = true  m._i = input  m._f = TRAVELPORT_FORMAT  m._tzm = offset  m._isUTC = true  m._locale = FAKE._locale  m._d = date  m._isValid = true  m._offset = offset  return m } 

我们工作的最后一步是找出如何解析时间戳的offset值。 事实证明,这始终是行中相同的位置。 结果,我们能够优化这一点:

 function parseTravelportDateOffset(input: string) {  const hrs = +input.slice(23, 26)  const mins = +input.slice(27, 29)  return hrs * 60 + (hrs < 0 ? -mins : mins) } 

将所有内容放在一起后,会发生以下情况:

 const FAKE = moment() const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ' function parseTravelportDateOffset(input: string) {  const hrs = +input.slice(23, 26)  const mins = +input.slice(27, 29)  return hrs * 60 + (hrs < 0 ? -mins : mins) } /** *     ISO-8601,    : * - "2019-12-03T12:30:00.000-07:00" */ export function parseTravelportTimestamp(input: string): moment {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  const offset = parseTravelportDateOffset(input)  const date = new Date(input.slice(0, 23))  m._isAMomentObject = true  m._i = input  m._f = TRAVELPORT_FORMAT  m._tzm = offset  m._isUTC = true  m._locale = FAKE._locale  m._d = date  m._isValid = true  m._offset = offset  return m } 

性能测试


我们使用基准npm模块测试了所得解决方案的性能。 这是基准代码:

 const FAKE = moment() const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ' function parseTravelportDateOffset(input: string) {  const hrs = +input.slice(23, 26)  const mins = +input.slice(27, 29)  return hrs * 60 + (hrs < 0 ? -mins : mins) } /** *     ISO-8601,    : * - "2019-12-03T12:30:00.000-07:00" */ export function parseTravelportTimestamp(input: string): moment {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  const offset = parseTravelportDateOffset(input)  const date = new Date(input.slice(0, 23))  m._isAMomentObject = true  m._i = input  m._f = TRAVELPORT_FORMAT  m._tzm = offset  m._isUTC = true  m._locale = FAKE._locale  m._d = date  m._isValid = true  m._offset = offset  return m } 

这是我们性能研究的结果:

 $ node fastMoment.bench.js moment#parseZone x 21,063 ops/sec ±7.62% (73 runs sampled) moment#parseZone (with format) x 24,620 ops/sec ±6.11% (71 runs sampled) fast#parseTravelportTimestamp x 1,357,870 ops/sec ±5.24% (79 runs sampled) Fastest is fast#parseTravelportTimestamp 

事实证明,我们设法将时间戳分析速度提高了约64倍。 但是,这如何影响系统的实际运行? 这是概要分析的结果。


parseTravelportTimestamp的总执行时间少于40毫秒。

结果简直令人惊讶:我们从3.3秒开始,一直到解析日期,最后不到40毫秒。

总结


当我们开始在平台上工作时,我们只需要解决大量问题。 我们只知道自己在重复自己:“先让它起作用,但您可以稍后进行优化”。

在过去的几年中,我们项目的复杂性急剧增加。 幸运的是,现在我们来到了可以进行“口头禅”第二部分-优化的地方。

图书馆解决方案帮助该项目发展到今天。 但是,我们面临着“图书馆”问题。 解决该问题后,我们了解到,通过创建自己的机制来满足需求,我们可以在消耗系统资源方面使代码“更轻松”,并为用户节省了宝贵的时间。

亲爱的读者们! 您是否遇到过与本文讨论的问题类似的问题?


Source: https://habr.com/ru/post/zh-CN468411/


All Articles