The Codest
  • Sobre nós
  • Serviços
    • Desenvolvimento de software
      • Desenvolvimento de front-end
      • Desenvolvimento backend
    • Staff Augmentation
      • Programadores Frontend
      • Programadores de back-end
      • Engenheiros de dados
      • Engenheiros de nuvem
      • Engenheiros de GQ
      • Outros
    • Aconselhamento
      • Auditoria e consultoria
  • Indústrias
    • Fintech e Banca
    • E-commerce
    • Adtech
    • Tecnologia da saúde
    • Fabrico
    • Logística
    • Automóvel
    • IOT
  • Valor para
    • CEO
    • CTO
    • Gestor de entregas
  • A nossa equipa
  • Case Studies
  • Saber como
    • Blogue
    • Encontros
    • Webinars
    • Recursos
Carreiras Entrar em contacto
  • Sobre nós
  • Serviços
    • Desenvolvimento de software
      • Desenvolvimento de front-end
      • Desenvolvimento backend
    • Staff Augmentation
      • Programadores Frontend
      • Programadores de back-end
      • Engenheiros de dados
      • Engenheiros de nuvem
      • Engenheiros de GQ
      • Outros
    • Aconselhamento
      • Auditoria e consultoria
  • Valor para
    • CEO
    • CTO
    • Gestor de entregas
  • A nossa equipa
  • Case Studies
  • Saber como
    • Blogue
    • Encontros
    • Webinars
    • Recursos
Carreiras Entrar em contacto
Seta para trás VOLTAR
2020-11-25
Desenvolvimento de software

Alguns truques para acelerar a sua aplicação JavaScript

The Codest

Bartosz Slysz

Software Engineer

Com o avanço da tecnologia dos browsers, as aplicações Web começaram a transferir cada vez mais lógica para o front-end, aliviando assim o servidor e reduzindo o número de operações que este tem de efetuar. Nos CRUDs básicos, o papel do servidor resume-se à autorização, validação, comunicação com as bases de dados e à lógica comercial necessária. O resto da lógica de dados, como se vê, pode ser facilmente tratado pelo código responsável pela representação da aplicação no lado da IU.

Neste artigo, vou tentar mostrar-vos alguns exemplos e padrões que ajudarão a manter a nossa código eficiente, limpo e rápido.

Antes de nos aprofundarmos em exemplos específicos - neste artigo, gostaria de me concentrar apenas em mostrar exemplos que, na minha opinião, podem afetar a velocidade da aplicação de uma forma surpreendente. No entanto, isto não significa que a utilização de soluções mais rápidas seja a melhor escolha em todos os casos possíveis. As dicas abaixo devem ser tratadas como algo que deve ser analisado quando a nossa aplicação está a correr lentamente, por exemplo, em produtos que requerem a renderização de jogos ou gráficos mais avançados no ecrã, operações de vídeo ou actividades que pretende sincronizar em tempo real o mais rapidamente possível.

Em primeiro lugar - métodos Array.prototype

Baseamos uma grande parte da lógica da aplicação em arrays - o seu mapeamento, ordenação, filtragem, soma de elementos, etc. De uma forma fácil, transparente e natural, utilizamos os seus métodos incorporados que permitem simplesmente nós para efetuar vários tipos de cálculos, agrupamentos, etc. Funcionam de forma semelhante em cada instância - como argumento, passamos uma função em que, na maioria dos casos, o valor do elemento, o índice e a matriz são empurrados alternadamente durante cada iteração. A função especificada é executada para cada elemento da matriz e o resultado é interpretado de forma diferente consoante o método. Não vou entrar em pormenores sobre os métodos do protótipo Array.prototype, pois quero concentrar-me na razão pela qual funciona lentamente num grande número de casos.

Os métodos de matriz são lentos porque executam uma função para cada elemento. Uma função chamada da perspetiva do motor tem de preparar uma nova chamada, fornecer o âmbito apropriado e muitas outras dependências, o que torna o processo muito mais longo do que repetir um bloco de código específico num âmbito específico. E este é provavelmente o conhecimento de base suficiente para nos permitir compreender o exemplo seguinte:

(() => {
const randomArray = [...Array(1E6).keys()].map(() => ({ value: Math.random() }));

consola.time('Soma por redução');
const reduceSum = randomArray
    .map(({ valor }) => valor)
    .reduce((a, b) => a + b);
consola.timeEnd('Soma por redução');

console.time('Soma por loop for');
let forSum = randomArray[0].value;
for (let index = 1; index < randomArray.length; index++) {
    forSum += randomArray[index].value;
}
console.timeEnd('Soma por loop for');

console.log(reduzirSoma === paraSoma);

})();

Sei que este teste não é tão fiável como os benchmarks (voltaremos a eles mais tarde), mas acende uma luz de aviso. Para um caso aleatório no meu computador, verifica-se que o código com o ciclo for pode ser cerca de 50 vezes mais rápido se comparado com o mapeamento e posterior redução de elementos que obtêm o mesmo efeito! Trata-se de operar sobre um objeto estranho criado apenas para atingir um objetivo específico de computação. Por isso, vamos criar algo mais legítimo para sermos objectivos em relação aos métodos Array:

(() => {
const randomArray = [...Array(1E6).keys()].map(() => ({ value: Math.random() }));

consola.time('Soma por redução');
const reduceSum = randomArray
    .reduce((a, b) => ({ valor: a.valor + b.valor })).value
consola.timeEnd('Soma por redução');

consola.time('Soma por loop for');
let forSum = randomArray[0].value;
for (let index = 1; index < randomArray.length; index++) {
    forSum += randomArray[index].value;
}
console.timeEnd('Soma por loop for');

console.log(reduceSum === forSum);

})();

Sei que este teste não é tão fiável como os benchmarks (voltaremos a eles mais tarde), mas acende uma luz de aviso. Para um caso aleatório no meu computador, verifica-se que o código com o ciclo for pode ser cerca de 50 vezes mais rápido se comparado com o mapeamento e redução de elementos que obtêm o mesmo efeito! Isto deve-se ao facto de, neste caso específico, obter a soma utilizando o método reduzir requer o mapeamento da matriz para os valores puros que queremos resumir. Então, vamos criar algo mais legítimo para sermos objetivos sobre os métodos Array:

(() => {
const randomArray = [...Array(1E6).keys()].map(() => ({ value: Math.random() }));

consola.time('Soma por redução');
const reduceSum = randomArray
    .reduce((a, b) => ({ valor: a.valor + b.valor })).value
consola.timeEnd('Soma por redução');

consola.time('Soma por loop for');
let forSum = randomArray[0].value;
for (let index = 1; index < randomArray.length; index++) {
    forSum += randomArray[index].value;
}
console.timeEnd('Soma por loop for');

console.log(reduceSum === forSum);

})();

E, como se vê, o nosso aumento de 50x diminuiu para 4x. Peço desculpa se ficou desiludido! Para sermos objectivos até ao fim, vamos analisar novamente os dois códigos. Em primeiro lugar, diferenças aparentemente inocentes duplicaram a queda na nossa complexidade computacional teórica; em vez de primeiro mapear e depois somar elementos puros, continuamos a operar sobre objectos e um campo específico, para finalmente obter a soma que nos interessa. O problema surge quando outro programador dá uma vista de olhos ao código - nesse caso, em comparação com os códigos mostrados anteriormente, este último perde a sua abstração em algum ponto.

Isto deve-se ao facto de, desde a segunda operação, operarmos sobre um objeto estranho, com o campo que nos interessa e o segundo objeto padrão da matriz iterada. Não sei o que pensa sobre isto, mas na minha perspetiva, no segundo exemplo de código, a lógica do ciclo for é muito mais clara e intencional do que esta estranha redução. E mesmo assim, apesar de já não ser o mítico 50, continua a ser 4 vezes mais rápido em termos de tempo de computação! Como cada milissegundo é valioso, a escolha neste caso é simples.

O exemplo mais surpreendente

A segunda coisa que queria comparar diz respeito ao método Math.max ou, mais precisamente, encher um milhão de elementos e extrair os maiores e os mais pequenos. Preparei o código, os métodos para medir o tempo também, depois iniciei o código e recebi um erro muito estranho - o tamanho da pilha foi excedido. Aqui está o código:

(() => {
const randomValues = [...Array(1E6).keys()].map(() => Math.round(Math.random() * 1E6) - 5E5);

console.time('Math.max com operador de dispersão ES6');
const maxBySpread = Math.max(...randomValues);
consola.timeEnd('Math.max com operador de dispersão ES6');

consola.time('Math.max com o ciclo for');
let maxByFor = randomValues[0];
for (let index = 1; index  maxByFor) {
        maxByFor = randomValues[index];
    }
}
console.timeEnd('Math.max com loop for');

consola.log(maxByFor === maxBySpread);

})();
(() => {
const randomValues = [...Array(1E6).keys()].map(() => Math.round(Math.random() * 1E6) - 5E5);

console.time('Math.max com operador de dispersão ES6');
const maxBySpread = Math.max(...randomValues);
consola.timeEnd('Math.max com operador de dispersão ES6');

consola.time('Math.max com o ciclo for');
let maxByFor = randomValues[0];
for (let index = 1; index  maxByFor) {
        maxByFor = randomValues[index];
    }
}
console.timeEnd('Math.max com loop for');

consola.log(maxByFor === maxBySpread);

})();

Acontece que os métodos nativos usam recursão, que na v8 é limitada pelas pilhas de chamadas e o seu número depende do ambiente. Isto é algo que me surpreendeu bastante, mas leva a uma conclusão: o método nativo é mais rápido, desde que o nosso array não exceda um certo número mágico de elementos, que no meu caso acabou por ser 125375. Para este número de elementos, o resultado do for foi 5x mais rápido se comparado com o loop. No entanto, acima do número de elementos mencionado, o ciclo for ganha definitivamente - ao contrário do adversário, permite-nos obter resultados corretos.

Recursão

O conceito que quero mencionar neste parágrafo é o de recursão. No exemplo anterior, vimos isso no método Math.max e na dobragem de argumentos, onde se verificou que é impossível obter um resultado para chamadas recursivas que excedam um número específico devido à limitação do tamanho da pilha.

Vamos ver agora como é a recursão no contexto de código escrito em JS, e não com métodos embutidos. Talvez a coisa mais clássica que podemos mostrar aqui é, obviamente, encontrar o enésimo termo na sequência de Fibonacci. Então, vamos escrever isso!

(() => {
const fiboIterative = (n) => {
let [a, b] = [0, 1];

    for (let i = 0; i  {
    se(n < 2) {
        retorna n;
    }

    return fiboRecursivo(n - 2) + fiboRecursivo(n - 1);
};

consola.time('Sequência de Fibonacci por laço for');
const resultIterative = fiboIterative(30);
consola.timeEnd('Sequência de Fibonacci por laço de for');

consola.time('Sequência de Fibonacci por recursão');
const resultRecursive = fiboRecursive(30);
consola.timeEnd('Sequência de Fibonacci por recursão');

consola.log(resultadoRecursivo === resultadoIterativo);

})();

Ok, neste caso particular de calcular o 30º item da sequência no meu computador, obtemos o resultado em cerca de 200x menos tempo com o algoritmo iterativo.

Há, no entanto, uma coisa que pode ser rectificada no algoritmo recursivo - como se verifica, funciona de forma muito mais eficiente quando utilizamos uma tática chamada recursão de cauda. Isto significa que passamos o resultado que obtivemos na iteração anterior usando argumentos para chamadas mais profundas. Isto permite-nos reduzir o número de chamadas necessárias e, consequentemente, acelera o resultado. Vamos corrigir o nosso código em conformidade!

(() => {
const fiboIterative = (n) => {
let [a, b] = [0, 1];

    for (let i = 0; i  {
    se(n === 0) {
        retorna primeiro;
    }

    return fiboTailRecursive(n - 1, second, first + second);
};

consola.time('Sequência de Fibonacci por laço para');
const resultIterative = fiboIterative(30);
consola.timeEnd('Sequência de Fibonacci por loop');

console.time('Sequência de Fibonacci por recursão de cauda');
const resultRecursive = fiboTailRecursive(30);
consola.timeEnd('Sequência de Fibonacci por recursão da cauda');

consola.log(resultadoRecursivo === resultadoIterativo);

})();

Algo que eu não esperava aconteceu aqui - o resultado do algoritmo de recursão de cauda foi capaz de entregar o resultado (computar o 30º elemento de uma sequência) quase duas vezes mais rápido que o algoritmo iterativo em alguns casos. Não tenho a certeza absoluta se isto se deve à otimização da recursão da cauda por parte da v8 ou à falta de otimização do ciclo for para este número específico de iterações, mas o resultado é inequívoco - a recursão da cauda ganha.

Isto é estranho porque, essencialmente, o ciclo for impõe muito menos abstração nas actividades de computação de nível inferior, e pode dizer-se que está mais próximo da operação básica do computador. No entanto, os resultados são inegáveis - a recursão inteligentemente concebida acaba por ser mais rápida do que a iteração.

Utilizar instruções de chamada assíncronas sempre que possível

Gostaria de dedicar o último parágrafo a uma breve chamada de atenção para um método de execução de operações que também pode afetar grandemente a velocidade da nossa aplicação. Como deve saber, JavaScript é uma linguagem single-threaded que mantém todas as operações com o mecanismo de event-loop. Trata-se de um ciclo que se repete continuamente e todas as etapas deste ciclo são acções específicas.

Para tornar este ciclo rápido e permitir que todos os ciclos esperem menos pela sua vez, todos os elementos devem ser tão rápidos quanto possível. Evitar executar operações longas na thread principal - se algo demorar muito tempo, tentar mover esses cálculos para o WebWorker ou dividi-los em partes que sejam executadas de forma assíncrona. Pode tornar algumas operações mais lentas, mas melhora todo o ecossistema de JS, incluindo as operações de IO, como o tratamento de movimentos do rato ou pedidos HTTP pendentes.

Resumo

Em conclusão, tal como mencionado anteriormente, a procura de milissegundos que podem ser poupados através da seleção de um algoritmo pode revelar-se inútil em alguns casos. Por outro lado, negligenciar estes aspectos em aplicações que requerem um funcionamento suave e resultados rápidos pode ser fatal para a sua aplicação. Nalguns casos, para além da velocidade do algoritmo, deve ser feita uma pergunta adicional - a abstração é operada ao nível correto? O programador que lê o código será capaz de o compreender sem problemas?

A única forma é garantir o equilíbrio entre desempenho, facilidade de implementação e abstração adequada, e ter a certeza de que o algoritmo funciona corretamente tanto para pequenas como para grandes quantidades de dados. A forma de o fazer é bastante simples - seja inteligente, considere os diferentes casos ao conceber o algoritmo e organize-o de modo a comportar-se da forma mais eficiente possível para execuções médias. Além disso, é aconselhável conceber testes - certifique-se de que o algoritmo devolve a informação adequada para diferentes dados, independentemente da forma como funciona. Cuide das interfaces corretas - para que tanto a entrada como a saída dos métodos sejam legíveis, claras e reflictam exatamente o que estão a fazer.

Mencionei anteriormente que voltarei à questão da fiabilidade da medição da velocidade dos algoritmos nos exemplos acima. Medi-los com console.time não é muito fiável, mas reflecte melhor o caso de utilização padrão. De qualquer forma, apresento os benchmarks abaixo - alguns deles parecem um pouco diferentes de uma única execução devido ao facto de os benchmarks simplesmente repetirem uma determinada atividade num determinado momento e utilizarem a otimização v8 para os loops.

  • https://jsben.ch/KhAqb - reduzir vs for loop
  • https://jsben.ch/F4kLY - redução optimizada vs for loop
  • https://jsben.ch/MCr6g - Math.max vs for loop
  • https://jsben.ch/A0CJB - fibo recursivo vs fibo iterativo
  • https://jsben.ch/NFLsl - fibo recursivo de cauda vs fibo iterativo 

E é tudo - divirtam-se com a vossa pirataria!

Ler mais:

Como melhorar as aplicações Vue.js? Algumas dicas práticas

Formas de aumentar o desempenho do Rails

Como escrever um código bom e de qualidade?

Artigos relacionados

Ilustração de uma aplicação de cuidados de saúde para smartphone com um ícone de coração e um gráfico de saúde em ascensão, com o logótipo The Codest, representando soluções digitais de saúde e HealthTech.
Desenvolvimento de software

Softwares para o setor de saúde: Tipos, casos de uso

As ferramentas em que as organizações de cuidados de saúde confiam atualmente não se assemelham em nada às fichas de papel de há décadas atrás. O software de cuidados de saúde apoia agora os sistemas de saúde, os cuidados aos doentes e a prestação de cuidados de saúde modernos em...

OCODEST
Ilustração abstrata de um gráfico de barras em declínio com uma seta ascendente e uma moeda de ouro que simboliza a eficiência ou a poupança de custos. O logótipo The Codest aparece no canto superior esquerdo com o slogan "In Code We Trust" sobre um fundo cinzento claro
Desenvolvimento de software

Como dimensionar a sua equipa de desenvolvimento sem perder a qualidade do produto

Aumentar a sua equipa de desenvolvimento? Saiba como crescer sem sacrificar a qualidade do produto. Este guia cobre sinais de que é hora de escalar, estrutura da equipe, contratação, liderança e ferramentas - além de como o The Codest pode...

OCODEST
Desenvolvimento de software

Construir aplicações Web preparadas para o futuro: ideias da equipa de especialistas do The Codest

Descubra como o The Codest se destaca na criação de aplicações web escaláveis e interactivas com tecnologias de ponta, proporcionando experiências de utilizador perfeitas em todas as plataformas. Saiba como a nossa experiência impulsiona a transformação digital e o negócio...

OCODEST
Desenvolvimento de software

As 10 principais empresas de desenvolvimento de software sediadas na Letónia

Saiba mais sobre as principais empresas de desenvolvimento de software da Letónia e as suas soluções inovadoras no nosso último artigo. Descubra como estes líderes tecnológicos podem ajudar a elevar o seu negócio.

thecodest
Soluções para empresas e escalas

Fundamentos do desenvolvimento de software Java: Um Guia para Terceirizar com Sucesso

Explore este guia essencial sobre o desenvolvimento de software Java outsourcing com sucesso para aumentar a eficiência, aceder a conhecimentos especializados e impulsionar o sucesso do projeto com The Codest.

thecodest

Subscreva a nossa base de conhecimentos e mantenha-se atualizado sobre os conhecimentos do sector das TI.

    Sobre nós

    The Codest - Empresa internacional de desenvolvimento de software com centros tecnológicos na Polónia.

    Reino Unido - Sede

    • Office 303B, 182-184 High Street North E6 2JA
      Londres, Inglaterra

    Polónia - Pólos tecnológicos locais

    • Parque de escritórios Fabryczna, Aleja
      Pokoju 18, 31-564 Cracóvia
    • Embaixada do Cérebro, Konstruktorska
      11, 02-673 Varsóvia, Polónia

      The Codest

    • Início
    • Sobre nós
    • Serviços
    • Case Studies
    • Saber como
    • Carreiras
    • Dicionário

      Serviços

    • Aconselhamento
    • Desenvolvimento de software
    • Desenvolvimento backend
    • Desenvolvimento de front-end
    • Staff Augmentation
    • Programadores de back-end
    • Engenheiros de nuvem
    • Engenheiros de dados
    • Outros
    • Engenheiros de GQ

      Recursos

    • Factos e mitos sobre a cooperação com um parceiro externo de desenvolvimento de software
    • Dos EUA para a Europa: Porque é que as empresas americanas decidem mudar-se para a Europa?
    • Comparação dos centros de desenvolvimento da Tech Offshore: Tech Offshore Europa (Polónia), ASEAN (Filipinas), Eurásia (Turquia)
    • Quais são os principais desafios dos CTOs e dos CIOs?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Website terms of use

    Direitos de autor © 2026 por The Codest. Todos os direitos reservados.

    pt_PTPortuguese
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench pl_PLPolish arArabic it_ITItalian jaJapanese es_ESSpanish nl_NLDutch etEstonian elGreek cs_CZCzech pt_PTPortuguese