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 = {}
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 = {}
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) } export function parseTravelportTimestamp(input: string): moment { const 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) } export function parseTravelportTimestamp(input: string): moment { const 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?
