javascript - pagina - tiempo de carga web
Obtenga 100 puntos en Google PageSpeed Insights con Meteor(es decir, una página de inicio de aplicación web) (1)
Mi cliente exige una aplicación web representada en el lado del cliente, rica en características, que al mismo tiempo obtiene 100/100 en Google PageSpeed Insights y se procesa muy rápido en la primera carga con un caché vacío. Ella quiere usar el mismo sitio como una aplicación web y como una página de aterrizaje y tiene un motor de búsqueda que rastrea fácilmente todo el sitio con un buen SEO.
¿Es esto posible usando Meteor? ¿Cómo puede hacerse esto?
Sí, esto es posible y fácil usando Meteor 1.3, algunos paquetes adicionales y un truco menor.
Ver bc-real-estate-math.com para un ejemplo. (este sitio solo puntúa 97 porque no he dimensionado las imágenes y Analytics y el seguimiento de FB tienen vidas cortas de caché)
Tradicionalmente, una plataforma renderizada del lado del cliente como Meteor fue lenta en las primeras cargas con un caché vacío debido a la gran carga útil de Javascript. La representación del lado del servidor (usando React) de la primera página casi resuelve esto, excepto que Meteor out-of-the-box no admite Javascript asincrónico o CSS en línea, lo que ralentiza su primer render y mata su puntaje de Google PageSpeed Insights (y argumentan como es posible que sobre esa métrica, afecte los precios de AdWord de mis clientes y, por lo tanto, lo optimizo para ello).
Esto es lo que puede lograr con la configuración de esta respuesta:
- Tiempo de renderizado rápido en la memoria caché vacía, como 500 ms
- Sin "flash de contenido con estilo"
- Puntuación 100/100 en Google PageSpeed Insights
- Uso de cualquier webfont sin matar su puntaje de PageSpeed
- Control completo de SEO, incluido el título de la página y meta
- Integración perfecta con Google Analytics y Facebook Pixels que registra con precisión cada vista de página independientemente de la representación del servidor o del lado del cliente
- El robot de búsqueda de Google y otros rastreadores ven el HTML real de todas sus páginas inmediatamente sin ejecutar secuencias de comandos
- Maneja sin problemas #hash URLs para desplazarse a partes de una página
- Use un número pequeño (como <30) de caracteres de fuentes de íconos sin agregar solicitudes o dañando el puntaje de velocidad
- Escala hasta cualquier tamaño de JavaScript sin afectar la experiencia de la página de destino
- Toda la maravilla regular de una aplicación web Meteor completa
Lo que esta configuración no puede lograr:
- Los grandes marcos de CSS monolíticos comenzarán a matar tu puntaje de PageSpeed y ralentizar el tiempo de renderización. Bootstrap es lo más grande que puedes llegar antes de que comiences a ver problemas
- No puede evitar una fuente de flash-of-wrong y aún mantener 100/100 PageSpeed. La primera representación será la fuente segura para la web del cliente, la segunda representación usará la fuente que haya diferido anteriormente.
Esencialmente, lo que puedes hacer es:
- El cliente solicita cualquier url dentro de su sitio
- El servidor envía de vuelta un archivo HTML completo con CSS en línea, async Javascript y fuentes diferidas
- El cliente solicita imágenes (si las hay) y el servidor las envía
- El cliente ahora puede renderizar la página
- Fuentes diferidas (si las hay) llegan y la página puede volver a renderizarse
- La carga útil de naves madre de Javascript llega al fondo
- Meteorito arranca y tienes una aplicación web en pleno funcionamiento con todas las comodidades y sin penalizaciones en la primera carga
- Mientras le dé al usuario unas líneas de texto para leer y una bonita imagen para mirar, nunca notará la transición de la página HTML estática a la aplicación web en toda regla.
Cómo lograr esto
Usé Meteor 1.3 y estos paquetes adicionales:
- reaccionar
- reacción-dom
- reaccionar-enrutador
- react-router-ssr
- reaccionar-casco
- postcss
- autoprefixer
- mechones de nodo-meteoro
Reaccionar juega bien con la representación del lado del servidor, no he probado ningún otro motor de representación. El casco de reacción se usa para agregar y modificar fácilmente el <head>
de cada página tanto del lado del cliente como del lado del servidor (por ejemplo, se requiere configurar el título de cada página). Utilizo el autoprefixer para agregar todos los prefijos específicos del proveedor a mi CSS / SASS, ciertamente no es necesario para este ejercicio.
La mayoría del sitio es bastante sencillo siguiendo los ejemplos en los documentos reaccionar-enrutador, reac-router-ssr y reaccionar-casco. Consulte los documentos de esos paquetes para obtener detalles sobre ellos.
En primer lugar, un archivo muy importante que debería estar en un directorio Meteor compartido (es decir, no en una carpeta de servidor o cliente). Este código configura la representación del lado del servidor de React, la etiqueta <head>
, Google Analytics, el seguimiento de Facebook y se desplaza a #hash anclajes.
import { Meteor } from ''meteor/meteor'';
import { ReactRouterSSR } from ''meteor/reactrouter:react-router-ssr'';
import { Routes } from ''../imports/startup/routes.jsx'';
import Helmet from ''react-helmet'';
ReactRouterSSR.Run(
Routes,
{
props: {
onUpdate() {
hashLinkScroll();
// Notify the page has been changed to Google Analytics
ga(''send'', ''pageview'');
},
htmlHook(html) {
const head = Helmet.rewind();
html = html.replace(''<head>'', ''<head>'' + head.title + head.base + head.meta + head.link + head.script);
return html; }
}
},
{
htmlHook(html){
const head = Helmet.rewind();
html = html.replace(''<head>'', ''<head>'' + head.title + head.base + head.meta + head.link + head.script);
return html;
},
}
);
if(Meteor.isClient){
// Google Analytics
(function(i,s,o,g,r,a,m){i[''GoogleAnalyticsObject'']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,''script'',''https://www.google-analytics.com/analytics.js'',''ga'');
ga(''create'', ''UA-xxxxx-1'', ''auto'', {''allowLinker'': true});
ga(''require'', ''linker'');
ga(''linker:autoLink'', [''another-domain.com'']);
ga(''send'', ''pageview'');
// Facebook tracking
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version=''2.0'';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,''script'',''https://connect.facebook.net/en_US/fbevents.js'');
fbq(''init'', ''xxxx'');
fbq(''track'', "PageView");
fbq(''trackCustom'', ''LoggedOutPageView'');
}
function hashLinkScroll() {
const { hash } = window.location;
if (hash !== '''') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(() => {
$(''html, body'').animate({
scrollTop: $(hash).offset().top
}, 1000);
}, 100);
}
}
Así es como se configuran las rutas. Observe los atributos de título que luego se alimentan al casco de reacción para establecer el contenido <head>
.
import React from ''react'';
import { Router, Route, IndexRoute, browserHistory } from ''react-router'';
import App from ''../ui/App.jsx'';
import Homepage from ''../ui/pages/Homepage.jsx'';
import ExamTips from ''../ui/pages/ExamTips.jsx'';
export const Routes = (
<Route path="/" component={App}>
<IndexRoute
displayTitle="BC Real Estate Math Online Course"
pageTitle="BC Real Estate Math Online Course"
isHomepage
component={Homepage} />
<Route path="exam-preparation-and-tips">
<Route
displayTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam"
pageTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam"
path="top-math-mistakes-to-avoid"
component={ExamTips} />
</Route>
);
App.jsx - el componente externo de la aplicación. Observe la etiqueta <Helmet>
que establece algunas metaetiquetas y el título de la página según los atributos del componente de página específico.
import React, { Component } from ''react'';
import { Link } from ''react-router'';
import Helmet from "react-helmet";
export default class App extends Component {
render() {
return (
<div className="site-wrapper">
<Helmet
title={this.props.children.props.route.pageTitle}
meta={[
{name: ''viewport'', content: ''width=device-width, initial-scale=1''},
]}
/>
<nav className="site-nav">...
Un componente de página de ejemplo:
import React, { Component } from ''react'';
import { Link } from ''react-router'';
export default class ExamTips extends Component {
render() {
return (
<div className="exam-tips blog-post">
<section className="intro">
<p>
...
Cómo agregar fuentes diferidas.
Estas fuentes se cargarán después de la representación inicial y, por lo tanto, no retrasarán el tiempo de renderización. Creo que esta es la única forma de usar webfonts sin reducir la puntuación de PageSpeed. Sin embargo, conduce a un breve flash-of-wrong-font. Pon esto en un archivo de script incluido en el cliente:
WebFontConfig = {
google: { families: [ ''Open+Sans:400,300,300italic,400italic,700:latin'' ] }
};
(function() {
var wf = document.createElement(''script'');
wf.src = ''https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js'';
wf.type = ''text/javascript'';
wf.async = ''true'';
var s = document.getElementsByTagName(''script'')[0];
s.parentNode.insertBefore(wf, s);
})();
Si utiliza un servicio excelente como fontello.com y selecciona a mano solo los iconos que realmente necesita, puede incrustarlos en su CSS <head>
en línea y obtener íconos en la primera representación sin esperar un archivo de fuente grande.
El Hack
Eso es suficiente, pero el problema es que nuestras secuencias de comandos, CSS y fuentes se cargan de forma síncrona y ralentizan el procesamiento y matan nuestra puntuación de PageSpeed. Desafortunadamente, por lo que puedo decir, Meteor 1.3 no admite oficialmente ninguna forma de alinear el CSS o agregar el atributo async a las etiquetas del script. Debemos hackear unas pocas líneas en 3 archivos del paquete core-tip-generator del núcleo.
~ / .meteor / packages / boilerplate-generator / .1.0.8.4n62e6 ++ os + web.browser + web.cordova / os / boilerplate-generator.js
...
Boilerplate.prototype._generateBoilerplateFromManifestAndSource =
function (manifest, boilerplateSource, options) {
var self = this;
// map to the identity by default
var urlMapper = options.urlMapper || _.identity;
var pathMapper = options.pathMapper || _.identity;
var boilerplateBaseData = {
css: [],
js: [],
head: '''',
body: '''',
meteorManifest: JSON.stringify(manifest),
jsAsyncAttr: Meteor.isProduction?''async'':null, // <------------ !!
};
....
if (item.type === ''css'' && item.where === ''client'') {
if(Meteor.isProduction){ // <------------ !!
// Get the contents of aggregated and minified CSS files as a string
itemObj.inlineStyles = fs.readFileSync(pathMapper(item.path), "utf8");;
itemObj.inline = true;
}
boilerplateBaseData.css.push(itemObj);
}
...
~ / .meteor / packages / boilerplate-generator / .1.0.8.4n62e6 ++ os + web.browser + web.cordova / os / packages / boilerplate-generator / boilerplate_web.browser.html
<html {{htmlAttributes}}>
<head>
{{#each css}}
{{#if inline}}
<style>{{{inlineStyles}}}</style>
{{else}}
<link rel="stylesheet" type="text/css" class="__meteor-css__" href="{{../bundledJsCssUrlRewriteHook url}}">
{{/if}}
{{/each}}
{{{head}}}
{{{dynamicHead}}}
</head>
<body>
{{{body}}}
{{{dynamicBody}}}
{{#if inlineScriptsAllowed}}
<script type=''text/javascript''>__meteor_runtime_config__ = JSON.parse(decodeURIComponent({{meteorRuntimeConfig}}));</script>
{{else}}
<script {{../jsAsyncAttr}} type=''text/javascript'' src=''{{rootUrlPathPrefix}}/meteor_runtime_config.js''></script>
{{/if}}
{{#each js}}
<script {{../jsAsyncAttr}} type="text/javascript" src="{{../bundledJsCssUrlRewriteHook url}}"></script>
{{/each}}
{{#each additionalStaticJs}}
{{#if ../inlineScriptsAllowed}}
<script type=''text/javascript''>
{{contents}}
</script>
{{else}}
<script {{../jsAsyncAttr}} type=''text/javascript'' src=''{{rootUrlPathPrefix}}{{pathname}}''></script>
{{/if}}
{{/each}}
</body>
</html>
Ahora cuente el número de caracteres en esos 2 archivos que editó e ingrese los nuevos valores en el campo de longitud de las entradas de esos archivos en ~ / .meteor / packages / boilerplate-generator / .1.0.8.4n62e6 ++ os + web.browser + web.cordova / os.json
A continuación, elimine la carpeta project / .meteor / local para forzar a Meteor a usar el nuevo paquete core y reiniciar su aplicación (la recarga en caliente no funcionará). Solo verá los cambios en el modo de producción.
Esto es obviamente un truco y se romperá cuando Meteor actualizaciones. Espero publicar esto y obtener un poco de interés, trabajaremos hacia una mejor manera.
Que hacer
Cosas para mejorar sería:
- Evita el pirateo. Obtenga MDG para admitir oficialmente script asíncrono y CSS en línea de una manera flexible
- Permitir control granular sobre qué CSS se alinea y qué diferir
- Permitir control granular sobre qué JS asyn y qué sincronizar y qué alinear.