Meu trabalho na Atlassian envolve primariamente o desempenho do JIRA na nuvem. Descrevi parte do meu trabalho no artigo Hello, JIRA, mas existem muitos detalhes complexos que acabam ficando de fora.
Neste artigo vou descrever brevemente um importante aspecto sobre desempenho de grandes aplicações web e indicar um vídeo que explica o assunto do ponto de vista do Facebook em maiores detalhes.
O JIRA está na nuvem
Antes de qualquer coisa, é bom desmistificar algo sobre o JIRA.
Você pode conhecer esse produto como um sistema que sua empresa instala em algum servidor próprio. É verdade que muitas empresas possuem administradores dedicados aos produtos da Atlassian, afinal algumas possuem milhares de projetos e dezenas de milhares de usuários. O JIRA é conhecido como um produto maduro e indispensável para ter nos servidores sua empresa.
Entretanto, a Atlassian é uma empresa que aposta no futuro e está investindo fortemente para que o JIRA seja um produto tão maduro na nuvem (cloud) quanto é hoje quando instalado nos servidores das empresas.
Server vs. Cloud
O escopo do meu trabalho é melhorar a experiência dos usuários que usam o JIRA na nuvem e em geral isto significa fazer as páginas carregarem mais rápido. Porém, ao contrário do que se possa pensar, gasto muito pouco tempo pensando em como fazer um código Java executar mais rápido.
Um sistema usado na intranet de uma empresa geralmente enfrenta problemas relacionados a consultas no banco de dados, má implementação, deficiência na infraestrutura, arquitetura ruim e por aí vai.
Claro que tudo isso ainda vale para a Nuvem, mas neste caso existem problemas que podem ser maiores.
Primeiro, deve-se considerar a escala. Numa intranet você costuma ter poucos usuários. No pior dos casos alguns milhares, mas minha experiência diz que em 99% dos casos não passam de meia dúzia. Na nuvem, o número chega rapidamente aos milhões e os dados na casa dos Terabytes.
Segundo, a latência é um problema com gente acessando de todas as partes do mundo. Neste ponto, o tempo de carregamento de uma página pode ser mais afetado pela transferência de scripts, imagens e estilos do que pelo tempo de processamento do servidor em si.
Dependências, dependências, dependências
Programadores lidam com dependências o tempo todo. Seu código precisa de bibliotecas, seus scripts precisam de outros scripts, até você depende do conhecimento de outras pessoas para trabalhar.
Isto não é diferente quando falamos de recursos como scripts, imagens e estilos.
Em sites médios ou pequenos, ter uma dúzia de scripts não é um problema, mas aplicações grandes como o JIRA, cujo tamanho total de todos esses recursos alcança alguns bons megabytes e, literalmente, milhares de scripts são servidos para o usuário, isso torna-se um problema sério.
Otimizando os recursos de uma página web
Existem várias técnicas que podem ser aplicadas para melhor o tempo de carregamento de uma página, no que se refere ao download de todos os recursos que precisam ser carregados.
Você pode minificar os scripts e estilos através de um compilador que remove espaços e outros caracteres desnecessários. É uma espécie de compactação que mantém o funcionamento original do código e diminui o tamanho do arquivo consideravelmente.
É possível compactar com gzip os arquivos enviados ao usuário, diminuindo ainda mais a quantidade de bytes transferidos. Os navegadores geralmente enviam um cabeçalho HTTP dizendo de aceitam arquivos neste formato.
Evitando sobrecarregar a rede
Outro problema que envolve o protocolo HTTP/1.x é a necessidade de uma conexão por recurso transferido. Os navegadores geralmente permitem o estabelecimento de 4 a 8 conexões simultâneas por servidor. Se uma página do seu site tem 20 scripts, o navegador vai enfileirar o download num processo em cascata que pode levar muito mais tempo do que deveria. Cada abertura e fechamento de conexão leva tempo e a latência de rede multiplica o problema.
No HTTP/2 isto será resolvido em boa parte, pois a nova versão do protocolo permite multiplexação durante a transferência. Isso significa que vários arquivos podem ser transmitidos em apenas uma conexão e os pacotes podem ser enviados em qualquer ordem. Porém, ele não resolve o problema por completo de houverem centenas ou milhares de pequenos arquivos pois os cabeçalhos de cada arquivo ainda são transmitidos e vão gerar um overhead considerável.
Na verdade, o HTTP/2 permite um certo nível de compactação dos cabeçalhos usando um algoritmo próprio que não chega a ser tão eficiente, mas infelizmente a compactação usando gzip foi deixada de fora devido a uma vulnerabilidade de segurança que o formato introduz em conexões HTTPS e que foi descrito nos ataques conhecidos CRIME e BREACH.
Tudo isso leva aplicações sérias a concatenar os arquivos e servir apenas um grande script e um grande CSS. Já viu um app.js
por aí? Pois é, ferramentas como Webpack e Gulp estão aí para isso. Ao colocar tudo num arquivo, você economiza muito do overhead do protocolo TCP.
Entretanto, isso só funciona até certo ponto. Na medida em que sua aplicação fica maior, o tempo de carregamento da página começa a crescer muito, afinal o seu app.js
torna-se gigantesco. Imagine se o usuário precisa carregar 4 megabytes de JavaScript apenas para ver a primeira tela!
A partir deste ponto, torna-se necessário dividir o nosso grande e único recurso em alguns menores. A ideia é que uma página deve incluir somente o mínimo necessário para ela funcionar. Talvez possa incluir alguns recursos relacionados às funcionalidades mais usadas, de forma que o usuário consiga acessá-las imediatamente. Porém, os demais recursos podem ser carregados dinamicamente quando o usuário executa outras ações no sistema.
Ferramentas como o GWT e Webpack dão suporte a isto possibilitando definir estruturas no código JavaScript que carregam recursos dinamicamente. Quando o compilador encontra tais estruturas, ele entende que aquele código e tudo o que ele depende será carregado de forma assíncrona e coloca todos os recursos associados em separado do código principal.
Agora a aplicação é composta de um arquivo principal que permite o carregamento rápido da página e outros arquivos menores que são carregados no momento em que o usuário acessa as respectivas funcionalidades do sistema.
Mas nem tudo são flores. Aplicações grandes e complexas como o Facebook e o JIRA possuem muitos casos de uso diferentes e seus usuários usam o sistema de muitas e variadas formas. Pense no trabalho de decidir como dividir a base de código, como e em que ordem os arquivos serão carregados, quais funcionalidades são mais importantes e quais são menos usadas e por aí vai. Isto é algo que acaba sendo feito de forma iterativa e que exige análise apurada das estatísticas de uso do sistema.
Depois de trabalhar com isso, eu passei cada vez mais a aceitar aquela mensagem que existe em quase todo programa conhecido pedindo permissão para enviar estatísticas anônimas. 😛
Usando CDN
Por fim, supondo que você tem seus scripts organizados, compactados e tudo o mais. Ainda assim você não vai querer sobrecarregar seu servidor com milhões de requisições estáticas e nem vai querer deixar o usuário do outro lado do mundo esperando por alguns míseros pacotes TCP que demoram a vida toda!
Colocar recursos em CDN é algo que já faz parte da vida normal dos desenvolvedores web que trabalham com alguma escala.
Entretanto, nem sempre é fácil fazer isto, principalmente em sistemas que evoluem muito rápido. Para cada nova versão, os recursos precisam ser enviados para o CDN antes da atualização.
Primeiro você precisa garantir que todos os recursos são realmente estáticos. Parece fácil, mas muita gente acaba caindo na tentação de usar recursos que são dinamicamente gerados, tal como scripts que são gerados via PHP ou JSP ao invés de parametrizados usando variáveis e atributos na página. Sistemas que suportam múltiplas linguagens são ainda mais complicados.
Além disso, você precisa ter um esquema de versionamento que funcione, afinal não vai querer incorrer em dois grandes perigos:
- Carregar versão errada dos recursos porque que a URL do recurso continua a mesma e o cache do seu usuário retorna a versão anterior
- Carregar a mesma versão novamente cada vez que você atualiza o sistema, mesmo que o conteúdo de algum arquivo não tenha efetivamente mudado
Isso não é fácil de conseguir. Atualmente, a solução que adotamos no JIRA é renomear os arquivos usando o hash SHA1 do seu conteúdo.
Continue aprendendo
Nos tópicos acima, descrevi resumidamente as principais técnicas que tenho utilizado referentes ao desempenho de aplicações web. Tentei deixar claro a razão porque cada uma é usada e o problema que ela resolve, além das complicações podem surgir para a correta adoção da prática.
Entretanto, apenas arranhei a superfície do que significa o assunto. Não fique desanimado se não compreendeu o funcionamento de algo ou se não fez sentido na sua cabeça. Provavelmente parte disse é porque eu não sou tão bom em organizar as ideias, outra parte é porque para entender certas coisas você precisa de um background mais amplo e também porque eu fui propositalmente superficial em muitos pontos.
Se você está lendo este artigo por curiosidade, talvez esteja satisfeito em entender os conceitos. Se, por outro lado, você está trabalhando com desempenho e tem interesse em se aprofundar, sugiro pesquisar à parte cada um dos tópicos que mencionei. Pode deixar comentários aqui que tentarei responder na medida do possível.
Por fim, deixo o vídeo que me deu a ideia de escrever este artigo. Está em inglês, mas o apresentador descreve uma boa parte do que mencionei aqui e tem uma grande área de intersecção com o meu trabalho.