Auditoria do Chrome 500: Parte 1. Aterragem

As Ferramentas do desenvolvedor do Chrome têm a guia Auditoria. Nele, encontra-se uma ferramenta chamada Lighthouse, que serve para analisar o desempenho da aplicação web.

imagem

Decidi recentemente testar um aplicativo e fiquei horrorizado com os resultados. Imediatamente em várias seções, a avaliação ocorreu na zona vermelha. Comecei a estudar o que havia de errado com minha inscrição. E encontrou nos resultados da análise uma grande lista de recomendações muito úteis, cumpriu-as e obteve 500 pontos. Como resultado, o aplicativo começou a rodar muito mais rápido e revisei vários conceitos sobre o método de criação de aplicativos. E neste artigo, quero compartilhar as soluções mais interessantes que encontrei.

Se você não conseguir instalar o chrome, poderá instalar o farol a partir das npm e trabalhar com ele no console.

No artigo, não comecei a comparar cada recomendação com uma seção específica; em vez disso, dividi as seções em soluções que apliquei e de que Ligthouse gostava. Isso não é tudo o que ele recomenda, é apenas o mais interessante. As recomendações restantes são muito simples e, como o SEO, são conhecidas por todos há muito tempo.

Desempenho


Seleção de servidor


Esse é o conselho mais comum, mas é esse o fundamento de toda a produtividade. Felizmente, encontrar uma boa solução é simples, é qualquer data center de Camada 3 ou Camada 4. Esse status em si não diz nada sobre velocidade, mas diz que os proprietários cuidaram da qualidade.

Inicialização de aplicativo


Uma vez havia apenas html nos navegadores. Então veio o javascript e a lógica de negócios. Hoje, existe tanta lógica no cliente que o html não consegue lidar com isso e não é mais necessário. Mas porque se o navegador não puder iniciar o carregamento do arquivo JavaScript, teremos que colocar um pequeno pedaço de html para iniciar nosso aplicativo.

Idealmente, deve ser algo como isto:

<!DOCTYPE html> <html lang="ru"> <head> <title> </title> <link rel="manifest" href="./manifest.webmanifest"> <link rel="shortcut icon" href="content/images/favicon.ico" type="image/x-icon"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width" /> <meta name="theme-color" content="#425566"> <meta name="Description" content=" "> </head> <body> <div id="loader"> loading </div> <script async> // todo:     </script> </body> </html> 

Não deve haver nenhum conteúdo nele, apenas o código necessário para inicializar o aplicativo, que carregará o próprio aplicativo e o conteúdo.

Este artigo não considera otimização para bots, mas direi que é mais fácil capturar um bot específico e fornecer o que um bot específico precisa. O próprio bot do Google entenderá tudo, desde o conteúdo que será carregado posteriormente.

Usar tela inicial


Todos nós estamos acostumados a exibir telas ao carregar em aplicativos móveis e até mesmo ao carregar o sistema operacional, mas poucas pessoas usam telas em um aplicativo da web. É isso que colocaremos no bloco do carregador para que o usuário não fique entediado enquanto o aplicativo estiver carregando.

Como tela inicial, como opção, você pode usar animação css ou apenas uma imagem, como é feito em telefones celulares. A única condição é que deve ser muito leve.

imagem

O que nós ganhamos? Usuários com Internet lenta receberão instantaneamente uma reação do site, eles não irão admirar a tela branca e se perguntarão se o site está funcionando ou não. Os usuários com Internet rápida provavelmente nem a verão, mas eles têm defasagens na Internet.

Como um exemplo interessante do uso de telas de apresentação , apresentarei o site da estação dock , onde uma onda agradável adorna um carregamento muito longo do site. De fato, é assim que as pessoas com Internet lenta verão seu aplicativo.

E imediatamente apresso-me a incomodar aqueles que pensam que salpicar uma tela pode enganar o Lighthouse e colocar uma aplicação pesada atrás dele. Ele vê tudo e não lhe dará uma boa nota para uma aplicação pesada.

Inicialização de aplicativo


Agora que estamos distraindo a atenção do usuário com fotos, é hora de baixar o aplicativo. Para fazer isso, inserimos o seguinte script dentro do bloco de scripts.

 // 1.  ServiceWorker,     PWA if (navigator.serviceWorker && !navigator.serviceWorker.controller) { navigator.serviceWorker.register('pwabuider-sw.js', { scope: './' }); } // 2.    [ "./content/font.css", "./content/grid.css" ].forEach(function(url){ var style = document.createElement("link"); style.href = url; style.rel = "stylesheet"; document.head.appendChild(style); }); // 3.    [ "./scripts/polyfills.min.js", //  vendors.min.js "./scripts/main.min.js" // spa  ].forEach(function(url){ const script = document.createElement("script"); script.src = url; script.async = false; document.head.appendChild(script); }); 

Em que consiste:

  1. Conexão PWA - consideraremos na seção correspondente abaixo. Você precisa conectá-lo o mais rápido possível, porque é possível que o pwa já tenha tudo o necessário para o site funcionar e não haverá mais solicitações ao servidor.
  2. Conectar estilos - conecte estilos conforme necessário. Idealmente, esse código não deveria existir e os estilos devem conectar seus componentes conforme necessário.
  3. Conectar scripts - conecte o programa. Ele deve consistir em apenas dois desses scripts. Todos os outros scripts (mapas, análises, bibliotecas) que não afetam a exibição da primeira tela (não a página inteira) são carregados após desenhar a primeira tela do aplicativo. O componente de análise já deve carregar a análise após o carregamento do programa. A qualidade da análise não será afetada por isso, e os sistemas de análise suportam o carregamento após o download do programa. Os cartões só devem ser imersos depois que o usuário os digitaliza e eles atingem a tela. Com bibliotecas de terceiros necessárias para componentes específicos funcionarem da mesma forma.

Como resultado, mudando um pouco as prioridades, obtemos uma renderização rápida do aplicativo. Assim, os usuários e os robôs de pesquisa estão satisfeitos com a velocidade e, ao mesmo tempo, não infringem as análises.

Carregamento e renderização preguiçosos


Um parâmetro muito importante é a rapidez com que a primeira tela é desenhada e o usuário pode começar a interagir com esta página. E aqui vale a pena usar as seguintes otimizações:

1. Renderização preguiçosa. É necessário desenhar apenas a parte da página em que o usuário está olhando, e a renderização de componentes pesados ​​ou imagens já deve ser feita quando o usuário pular para eles.

Uma boa solução aqui são os componentes lazy-block e lazy-img:

 <div> <p></p> <lazy-img src="..."/> </div> <lazy-block>   </lazy-block> <lazy-block>   </lazy-block> <lazy-block>   </lazy-block> 

O ponto é que eles monitorarão a rolagem do usuário e, se o componente cair na área da tela, ele será desenhado. Isso pode ser comparado com a técnica de rolagem virtual ( exemplo ), que é familiar a todos nas paredes das redes sociais. Podemos rolar para sempre, mas eles nunca diminuem a velocidade.

Mas não se esqueça do bot do Google, que vê spa, mas não rola a página inteira. Portanto, se você não tomar cuidado, ele não verá seu conteúdo.

2. Se algum dos componentes usar uma dependência externa, ele precisará carregá-lo, conforme necessário. Por exemplo, pode ser um bloco com mapas, gráficos ou gráficos 3D. E, recentemente, uma maneira de fazer isso no JS tem sido muito simples:

 class Demo { constructor() { this.init(); } private async init() { const module = await import('./external.mjs'); //   module.default(); module.doStuff(); } } 

Como resultado, o usuário carrega apenas o que ele precisa, o que economiza bastante recursos do usuário e do servidor.

Minimização de pacote


E ... sim, você não pensou nisso, não se trata de minificação no Terser (UglifyJS), mas de fornecer apenas o necessário para um navegador específico.

O fato é que os navegadores estão em constante evolução, eles têm uma nova API, os desenvolvedores estão começando a usá-la e, para compatibilidade com navegadores mais antigos, conectam polyfills e transpilers. Como resultado, surge o problema de que os usuários com os navegadores mais recentes, que são cerca de 80%, obtenham código projetado para usuários do IE11, transpilados e com polyfiles.

O problema com esse código é que ele contém muito texto extra e seu desempenho é 3 vezes menor (de acordo com minhas estimativas subjetivas) do que o original. É muito mais lógico criar vários pacotes para diferentes versões de navegadores. Um pacote com o código ES2017 para o Chrome 73 com um mínimo de polyfiles, um pacote com ES5 para o IE11 com um mínimo de polyfiles etc.

Eu escrevi sobre como coletar pacotes de versões diferentes ao mesmo tempo em um artigo anterior . E para selecionar a versão correta no navegador, modificamos levemente o script de conexão do programa:

 var esVersion = ".es2017"; try{ eval('"use strict"; class foo {}'); }catch(e){ esVersion = ".es5"; } [ "./scripts/polyfills" + esVersion + ".min.js", "./scripts/main" + esVersion + ".min.js" ].forEach(function(url){ const script = document.createElement("script"); script.src = url; script.async = false; document.head.appendChild(script); }); 

Como resultado, os usuários de navegadores modernos receberão o programa mais leve e produtivo, e os usuários do IE11 obterão o que merecem.

Outra maneira interessante de minificar
Uma biblioteca muito interessante para reduzir pacotes em 50% , infelizmente com resultados imprevisíveis.

Minimização de código


Um problema muito popular é quando os desenvolvedores começam a conectar tudo em que seus olhos caem. Como resultado, às vezes você pode assistir a programas com peso de 5 a 15 mb ou mais. Portanto, a escolha das bibliotecas deve ser abordada com sabedoria.

Em vez de estruturas pesadas como Angular ou React, é melhor escolher suas contrapartes mais leves: vue, pré-reagir, mithril, etc. Eles não são de forma alguma inferiores aos seus eminentes colegas, mas a economia no tamanho do pacote pode ser várias vezes.

Evite usar bibliotecas pesadas. Em vez de usar bibliotecas como jquery, lodash, moment, rxjs e qualquer outra com tamanho reduzido> 100kb, tente estudar os algoritmos mais profundamente e encontrar uma solução em JS nativo. Como regra, você pode escrever mais simples em um script nativo e se livrar de uma dependência pesada desnecessária.

Minificação de imagem


Provavelmente, todos os desenvolvedores de front-end conhecem o formato de imagem webp e também a necessidade de reduzir as imagens para o tamanho de exibição necessário. Mas, por alguma razão, quase todos os desenvolvedores ignoram isso. E a razão para isso, na minha opinião, é extremamente simples, as pessoas não entendem como isso é feito e aplicado em diferentes navegadores.

Portanto, aqui darei uma receita muito simples para resolver todos os problemas com fotos. Esta receita é baseada na ferramenta de processamento e conversão de imagens Sharp . Destaca-se por um pipeline muito ponderado, devido ao qual a velocidade de processamento de imagem é 30 a 40 vezes maior que a dos análogos. E o próprio tempo de montagem de centenas de imagens de grandes fontes em diferentes tamanhos e formatos é comparável à velocidade de montagem de um front-end moderno.

Para usar o Sharp, você precisa escrever um script, eu o uso em conjunto com a glob para pesquisar recursivamente imagens no diretório com as imagens de origem e oculto o próprio script do utilitário para executar tarefas gulp. Um exemplo da minha montagem:

 gulp.task('core-min-images', async () => { const fs = require('fs'); const path = require('path'); const glob = require('glob'); const sharp = require('sharp'); // 1.          glob const files = await new Promise((resolve, reject) => { glob('src/content/**/*.{jpeg,jpg,png}', {}, async (er, files) => { !er ? resolve(files) : reject(er); }); }); // 2.      let completed = 1; await Promise.all(files.map(async (file) => { const outFile = file.replace(/^src/, 'www'); const outDir = path.dirname(outFile); // 2.1.       if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } // 2.2.    const origin = sharp(file); // 2.3.     1920     //       jpg/png  webp    (80%) const size1920 = origin.resize({ width: 1920 }); await size1920.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-1920w.$1')); await size1920.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-1920w.webp')); // 2.4.    480   const size480 = origin.resize({ width: 480 }); await size480.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-480w.$1')); await size480.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-480w.webp')); // 2.5.    120   const size120 = origin.resize({ width: 120 }); await size120.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-120w.$1')); await size120.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-120w.webp')); // 2.6.      console.log(`Complete image ${completed++} of ${files.length}:`, file); })); }); 

Como resultado, obtemos imagens otimizadas para diferentes tamanhos de tela e navegadores diferentes de cada imagem de origem de tamanho grande. Agora precisamos aprender a usá-los. Aqui também tudo é simples, se anteriormente escrevemos assim:

 <img src="sample.jpg"/> 

Agora precisamos escrever assim:

 <picture> <source srcset="img/sample-480w.webp" type="image/webp"> <source srcset="img/sample-480w.jpg" type="image/jpeg"> <img src="img/sample-480w.jpg" alt=" !"> </picture> 

E então o próprio navegador escolherá o formato mais conveniente para ele. Você também pode adicionar esta opção com imagens responsivas:

 <picture> <source srcset="img/sample-480w.webp, img/sample-960w.webp 2x" type="image/webp"> <source srcset="img/sample-480w.jpg, img/sample-960w.webp 2x" type="image/jpeg"> <img src="img/sample-480w.jpg" alt=" !"> </picture> 

E levando em conta o fato de que agora é possível gerar imagens no estágio de montagem do aplicativo, todas as imagens terão o mesmo conjunto de formatos e resoluções, o que significa que podemos unificar essa lógica e ocultá-la atrás de algum componente, por exemplo, o mesmo <lazy-img src="img/sample.jpg"> .

Minificação de Estilo


Faça o download apenas dos estilos que usam seus componentes. Idealmente, quando os estilos estão vinculados aos componentes e são incorporados na casa somente quando o próprio componente é desenhado.

Minimize os nomes das classes. O tamanho dos seletores aninhados ou BEM nos estilos afeta negativamente o tamanho do seu aplicativo. Atualmente, ele está cheio de ferramentas que não geram estilos com seletores exclusivos: JSS, Componentes com estilo, Módulos CSS.

Minificação em casa


Todos conhecemos o html, mas poucos pensam que isso seja apenas uma abstração simples sobre uma árvore de objetos muito complexos. A cadeia de herança para o elemento div é a seguinte:

HTMLDivElement -> HTMLElement -> Element -> Node -> EventTarget

E cada objeto nesta cadeia tem 10 a 100 propriedades e métodos que consomem muita memória. E toda essa riqueza deve ser levada em consideração pelo mecanismo do DOM para criar a imagem que vemos. Portanto, tente não usar elementos em excesso na casa.

Minimize o HTML. Exclua tudo o que você usa para formatar html no momento da redação. O fato é que os espaços usados ​​ao escrever código no navegador também se transformam em objetos em casa:

TextNode -> Node -> EventTarget

Excluir comentários. Eles também são um elemento da casa e consomem muitos recursos:

Comment -> CharacterData -> Node -> EventTarget

O uso de mecanismos de modelo jsx pode ser uma boa prática. O fato é que, ao compilá-lo, ele se transforma em código js nativo que não gera espaços, comentários e nunca se engana em abrir e fechar tags.

Má prática, eu diria mesmo um pesadelo, é o facebook.com . Aqui estão os fragmentos html:

Snippet de página em HTML
 <!--  1 --> <div class=""> <div class="_42ef"> <div class="_25-w"> <div class="_17pg"> <div class="_1rwk"> <form class=" _129h"> <div class=" _3d2q _65tb _7c_r _4w79"> <div class="_5rp7"> <div class="_1p1t"> <div class="_1p1v" id="placeholder-77m1n" style="white-space: pre-wrap;">  ... </div> </div> </div> </div> <ul class="_1obb"> ...li... </ul> </form> </div> </div> </div> </div> </div> <!--  2 --> <div> <div> <div class="_3nd0"> <div class="_1mwp navigationFocus _395 _4c_p _5bu_ _34nd _21mu _5yk1" role="presentation" style="" id="js_u"> <div class="_5yk2" tabindex="-1"> <div class="_5rp7"> <div class="_1p1t" style=""> <div class="_1p1v" id="placeholder-6t6up" style="white-space: pre-wrap;">    ? </div> </div> <div class="_5rpb"> <div aria-autocomplete="list" aria-controls="js_1" aria-describedby="placeholder-6t6up" aria-multiline="true" class="notranslate _5rpu" contenteditable="true" data-testid="status-attachment-mentions-input" role="textbox" spellcheck="true" style="outline: none; user-select: text; white-space: pre-wrap; overflow-wrap: break-word;"> <div data-contents="true"> <div class="" data-block="true" data-editor="6t6up" data-offset-key="6b02n-0-0"> <div data-offset-key="6b02n-0-0" class="_1mf _1mj"> <span data-offset-key="6b02n-0-0"> <br data-text="true"> </span> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> 


Como você pode ver, um aninhamento de dez elementos é usado, mas esse aninhamento não funciona. O primeiro fragmento exibe apenas o texto "Escreva um comentário ..." e os ícones, o segundo "O que há de novo?". Como resultado de um uso não racional do DOM, todo o desempenho do mecanismo de modelagem React é simplesmente negado, e o site se torna um dos mais lentos que eu conheço.

Progressive Web App


Arquivo de manifesto


O PWA permite que você use seu aplicativo Web como aplicativo nativo. Quando você ativa o suporte no site, o botão para instalar o site no dispositivo (Windows, Android, iOS) aparece no menu do navegador, após o qual começa a se comportar como nativo e funciona offline, e tudo isso ignorando as lojas de aplicativos.

Ativar o suporte ao PWA no site é realmente muito simples. Basta incluir um link para o arquivo de manifesto na página html. O arquivo de manifesto pode ser gerado em pwabuilder.com .

Não vou parar em detalhes no processo de conexão, porque Esta seção é digna de um grande artigo separado e no Habré já existem artigos muito bons.

Trabalhador de serviço


A configuração do PWA não termina ao conectar o arquivo de manifesto, também é necessário conectar o ServiceWorker, que será responsável por trabalhar offline.

Um código de exemplo pode ser encontrado em pwabuilder.com :

 // This is the service worker with the Cache-first network const CACHE = "pwabuilder-precache"; const precacheFiles = [ /* Add an array of files to precache for your app */ ]; self.addEventListener("install", function (event) { console.log("[PWA Builder] Install Event processing"); console.log("[PWA Builder] Skip waiting on install"); self.skipWaiting(); event.waitUntil( caches.open(CACHE).then(function (cache) { console.log("[PWA Builder] Caching pages during install"); return cache.addAll(precacheFiles); }) ); }); // Allow sw to control of current page self.addEventListener("activate", function (event) { console.log("[PWA Builder] Claiming clients for current page"); event.waitUntil(self.clients.claim()); }); // If any fetch fails, it will look for the request in the cache and serve it from there first self.addEventListener("fetch", function (event) { if (event.request.method !== "GET") return; event.respondWith( fromCache(event.request).then( function (response) { // The response was found in the cache so we responde with it and update the entry // This is where we call the server to get the newest version of the // file to use the next time we show view event.waitUntil( fetch(event.request).then(function (response) { return updateCache(event.request, response); }) ); return response; }, function () { // The response was not found in the cache so we look for it on the server return fetch(event.request) .then(function (response) { // If request was success, add or update it in the cache event.waitUntil(updateCache(event.request, response.clone())); return response; }) .catch(function (error) { console.log("[PWA Builder] Network request failed and no cache." + error); }); } ) ); }); function fromCache(request) { // Check to see if you have it in the cache // Return response // If not in the cache, then return return caches.open(CACHE).then(function (cache) { return cache.match(request).then(function (matching) { if (!matching || matching.status === 404) { return Promise.reject("no-match"); } return matching; }); }); } function updateCache(request, response) { return caches.open(CACHE).then(function (cache) { return cache.put(request, response); }); } 

Como você pode ver no código, todas as respostas do servidor são armazenadas em cache, mas o cache não é usado online. E eles começam a ser usados ​​quando a conexão com o servidor se esgota. Assim, o usuário que navega no site pode não perceber o desaparecimento de curto prazo da Internet e, mesmo que a Internet tenha desaparecido por um longo tempo, o usuário ainda tem a oportunidade de mover dados já armazenados em cache.

O script acima é simples, mas adequado apenas para páginas de destino e é apenas um ponto de partida para escrever um trabalhador para um aplicativo Web mais sério. Mas mais sobre isso na segunda parte deste artigo. Além disso, a tecnologia é conveniente, pois não interrompe o trabalho em navegadores antigos, ou seja, nos navegadores no nível IE11, você não precisa reescrever a lógica, o modo offline simplesmente não funcionará.

Acessibilidade


Correção de atributos para pessoas com necessidades especiais


Existem muito poucas pessoas com saúde perfeita, mas infelizmente há muitas pessoas com problemas de saúde, incluindo a visão. E para facilitar o uso dessas pessoas pelo aplicativo da Web, basta seguir regras bastante simples:

  • Use cores contrastantes suficientes. Segundo estatísticas do Ministério da Saúde, 20% das pessoas têm problemas de visão. Um baixo contraste de sites complica apenas suas vidas e pessoas saudáveis ​​aumentam a fadiga.
  • Organize o índice de tabulação. Permite que você use o site sem um mouse e dispositivos de toque. O arranjo adequado de transições usando o teclado simplifica bastante o processo de preenchimento de formulários.
  • Atributo Aria-label nos links. Permite que os leitores de tela leiam texto dentro de um atributo.
  • O atributo alt nas imagens. Semelhante ao anterior. Além disso, ele exibirá texto se não for possível baixar a imagem.
  • O idioma do documento. Marque a tag html com o atributo com idioma lang = "código do idioma". Isso ajudará as ferramentas auxiliares configuradas corretamente para o trabalho.

Como você pode ver, os requisitos são poucos e simples de cumprir. Mas, por alguma razão, a maioria dos desenvolvedores ignora essas regras, mesmo quando se trata de sites especializados para pessoas com necessidades especiais.

Melhores práticas


Separe o aplicativo front-end do aplicativo do servidor


Primeiro, se você ainda estiver renderizando html no servidor, pare de fazer isso já.Transferir o processo de renderização para o cliente em duas ordens de grandeza reduz a carga no servidor e, como resultado, o custo de suporte ao aplicativo do servidor. E os clientes obtêm um aplicativo com reação instantânea a suas ações.

Segundo, separe o aplicativo SPA do cliente do aplicativo back-end. Você não mantém juntos o aplicativo de servidor e o aplicativo do Windows, o aplicativo Android e o aplicativo iOS. Portanto, o aplicativo da Web há muito tempo é um aplicativo independente que pode funcionar sem um servidor e até offline. O erro mais popular que vejo é quando uma estrutura de back-end como Spring ou Asp.Net está envolvida na distribuição de estática, incluindo o aplicativo SPA montado. É hora de parar de fazer isso e remover a estática e o SPA em um microsserviço separado e ocultá-la atrás de um servidor da web especializado para distribuir estática, por exemplo, nginx.

imagem

Como resultado, cada tecnologia fará o que deve e fará melhor. O Nginx distribuirá a estática com os cabeçalhos corretos e a velocidade máxima, o aplicativo do servidor preparará os dados para o cliente, o dispositivo do cliente coletará tudo juntos e exibirá para o usuário.

Configurando servidor proxy, HTTP / 2, gzip, cache


Seu aplicativo de back-end não deve se comunicar diretamente com o cliente; é melhor ocultá-lo atrás de portas especializadas, por exemplo, o servidor proxy Nginx. E nele você já pode configurar tudo o que é necessário para uma comunicação confortável entre o dispositivo cliente e o servidor.

  • SSL. SSL , , , Nginx. Nginx Asp.Net Core , .
  • GZIP . .
  • Cache . Get, Head , .
  • .

Novamente, devido ao fato de esta parte ser digna de um grande artigo separado, não descrevo todo o processo de configuração em detalhes, mas recomendo um site para gerar a configuração nginx nginxconfig.io .

SEO


Crie meta tags em html e use marcação semântica


Todo mundo já sabe disso e, como regra, eles o usam. Portanto, para corrigi-lo, basta olhar para a lista de comentários do Lighthouse e corrigi-lo.

O fim


À primeira vista, pode parecer que muitas informações aqui escritas são difíceis de observar, mas na verdade não são. Todas essas informações refletem o estado atual do desenvolvimento do front-end e o cumprimento de todas essas regras quase não leva tempo.

Este artigo não descreve como otimizar a área administrativa, o formulário e outras empresas, mas esta será a segunda parte.

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


All Articles