Uma história sobre como resolver o problema de desempenho do Moment.js

Moment.js é uma das bibliotecas JavaScript mais populares para analisar e formatar datas. O WhereTo usa o Node.js, portanto, para eles, o uso desta biblioteca foi uma ação completamente natural. Não foram esperados problemas com o uso do servidor Moment.js. No final, desde o início, eles usaram essa biblioteca no frontend para exibir datas e ficaram satisfeitos com seu trabalho. No entanto, o fato de a biblioteca ter um bom desempenho no cliente também não significava que não haveria problemas com o servidor.



O material, cuja tradução publicamos hoje, é dedicado à história de solução do problema de desempenho Moment.js.

Crescimento do projeto e declínio da produtividade


Recentemente, o número de registros de voo retornados pelo sistema WhereTo aumentou cerca de dez vezes. Então, enfrentamos uma queda muito forte no desempenho. Aconteceu que o ciclo de renderização, que levou menos de 100 milissegundos, agora leva mais de 3 segundos para exibir cerca de 5.000 resultados de pesquisa. Nossa equipe começou a pesquisa. Após várias sessões de criação de perfil, percebemos que mais de 99% desse tempo é gasto em uma única função chamada createInZone .


A função createInZone leva cerca de 3,3 segundos para concluir.

Continuando nossa investigação da situação, descobrimos que essa função é chamada pela função parseZone parseZone . Por que ela é tão lenta? Tínhamos a sensação de que a biblioteca Moment.js foi projetada para cenários de uso comum e, como resultado, tentará processar a sequência de entrada de várias maneiras. Talvez você deva limitar isso? Depois de ler a documentação, descobrimos que a função parseZone aceita um argumento opcional que especifica o formato da data:

 moment.parseZone(input, [format]) 

A primeira coisa que fizemos foi tentar usar a função parseZone transmitindo informações sobre o formato da data, mas isso, como mostraram os testes de desempenho, não levou a nada:

 $ 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) 

Embora agora a função parseZone funcione um pouco mais rápido, para nossas necessidades essa velocidade claramente não era suficiente.

Otimização específica do projeto


Usamos o Moment.js para analisar as datas recuperadas da API do nosso provedor (Travelport). Percebemos que ele sempre retorna dados no mesmo formato:

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

Sabendo disso, começamos a entender a estrutura interna do Moment.js para (como esperávamos) escrever uma função muito mais eficiente que produz os mesmos resultados.

Criando uma alternativa mais rápida ao parseZone


Para começar, precisamos descobrir a aparência dos objetos Moment.js. Era bem fácil de entender:

 > 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 } 

O próximo passo foi instanciar o Moment sem usar um construtor:

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

Agora parecia que tínhamos várias propriedades da instância do Moment que poderíamos definir (não vou entrar em detalhes de como descobrimos isso, mas se você olhar o código-fonte do Moment.js, entenderá):

 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 } 

A última etapa do nosso trabalho foi descobrir como analisar o valor de offset do registro de data e hora. Descobriu-se que esta é sempre a mesma posição na linha. Como resultado, conseguimos otimizar isso:

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

Aqui está o que aconteceu depois que juntamos tudo:

 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 } 

Testes de desempenho


Testamos o desempenho da solução resultante usando o módulo npm de referência. Aqui está o código de referência:

 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 } 

Aqui estão os resultados de nossa pesquisa de desempenho:

 $ 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 

Como se viu, conseguimos acelerar a análise dos registros de data e hora em cerca de 64 vezes. Mas como isso afetou a operação real do sistema? Aqui está o que aconteceu como resultado da criação de perfil.


O tempo total de execução de parseTravelportTimestamp é inferior a 40 ms.

Os resultados foram simplesmente surpreendentes: começamos com 3,3 segundos, analisando datas e chegamos a menos de 40 milissegundos.

Sumário


Quando começamos a trabalhar em nossa plataforma, tivemos que resolver apenas uma quantidade terrível de problemas. Sabíamos apenas que estávamos repetindo para nós mesmos: “Deixe funcionar primeiro, mas você poderá fazer a otimização posteriormente”.

Nos últimos anos, a complexidade do nosso projeto aumentou tremendamente. Felizmente, agora chegamos ao lugar onde podemos passar para a segunda parte do nosso "mantra" - otimização.

As soluções de bibliotecas ajudaram o projeto a chegar onde está hoje. Mas estamos diante de um problema de "biblioteca". Resolvendo isso, aprendemos que, ao criar nosso próprio mecanismo focado em nossas necessidades, podemos tornar o código "mais fácil" em termos de consumo de recursos do sistema e economizar tempo valioso para nossos usuários.

Caros leitores! Você encontrou problemas semelhantes ao discutido neste artigo?


Source: https://habr.com/ru/post/pt468411/


All Articles