2016-07-12 61 views
7

Il mio cliente richiede un'app web resa disponibile sul lato client ricca di funzionalità che, allo stesso tempo, ottiene 100/100 su Google PageSpeed ​​Insights e rende molto veloce al primo caricamento con una cache vuota. Vuole utilizzare lo stesso sito sia come web-app che come landing page e fare in modo che qualsiasi motore di ricerca esegua facilmente la scansione di tutto il sito con un buon SEO.Punteggio 100 su Google PageSpeed ​​Insights che utilizza Meteor (ad esempio una pagina di destinazione dell'app Web)

È possibile utilizzare Meteor? Come può essere fatto?

risposta

23

Sì, questo è possibile e facile utilizzando Meteor 1.3, alcuni pacchetti aggiuntivi e un hack minore.

Vedere bc-real-estate-math.com per un esempio. (questo sito segna solo 97 perché non ho ridimensionato le immagini e Analytics e FB hanno vita cache breve)

Tradizionalmente, una piattaforma resa client come Meteor era lenta sui primi carichi con una cache vuota a causa del grande carico utile di Javascript. Il rendering lato server (usando React) della prima pagina lo risolve quasi eccetto che Meteor out-of-the-box non supporta JavaScript asincrono o CSS in linea rallentando così il tuo primo rendering e uccidendo il tuo punteggio di Google PageSpeed ​​Insights (e argomenta come potresti avere a che fare con questo parametro, influisce sui prezzi di AdWord dei miei clienti e quindi lo ottimizzo).

Questo è quello che si può ottenere con l'installazione di questa risposta:

  • molto rapido time-to-first-render su Cache vuota, come 500ms
  • No "lampo di contenuti in stile"
  • Punteggio 100/100 su Google PageSpeed ​​Insights
  • Uso di qualsiasi webfont senza uccidere il tuo punteggio PageSpeed ​​
  • Controllo SEO completo incluso titolo della pagina e meta
  • Perfetta integrazione con Google Analytics e Facebook Pixel che registra con precisione ogni visualizzazione della pagina indipendentemente dal server o lato client di rendering
  • Google script di ricerca bot e altri crawler vedere tutto di beni HTML delle pagine immediatamente senza correre
  • gestisce senza soluzione di continuità URL #hash per scorrere fino a parti di una pagina
  • utilizzare un numero piccolo (come < 30) di icona di carattere caratteri senza aggiungere richieste o male velocità segnare
  • Scala fino a tutto il formato di Javascript, senza impactin g pagina di destinazione esperienza
  • Tutta la suggestione regolare di un pieno Meteor web-app

Ciò che questa impostazione non può raggiungere:

  • grandi quadri CSS monolitici cominceranno a uccidere il tuo punteggio Page Speed ​​e rallenta il time-to-first-rendering. Bootstrap è grande quanto puoi prima di iniziare a vedere i problemi
  • Non è possibile evitare un font flash-of-wrong e mantenere comunque 100/100 PageSpeed. Il primo rendering sarà il carattere sicuro per il Web del client, il secondo render utilizzerà il tipo di carattere che è stato differito in precedenza.

In sostanza ciò che si può fare accadere è:

  • client richiede un URL all'interno del tuo sito
  • Server invia indietro un file completo HTML con CSS in linea, async Javascript e differita font
  • Il client richiede le immagini (se presenti) e il server li invia
  • Il client può ora eseguire il rendering della pagina
  • font anticipate (se presenti) arrivano e la pagina potrebbe ri-renderizzare
  • Javascript nave madre payload arriva nel sfondo
  • Meteor stivali e si dispone di una web-app pienamente funzionante con tutte le campane e fischietti e nessuna penalità di primo carico
  • Finché si legge dando all'utente alcune righe di testo da leggere e una bella immagine a , non si noterà mai la transizione dalla pagina HTML statica a Web completo - app

Come per raggiungere questo

ho usato Meteor 1.3 e questi pacchetti aggiuntivi:

  • reagiscono
  • reagire-dom
  • reagire-router
  • reagire-router-SSR
  • react-helmet
  • postcss
  • autoprefixer
  • meteora-node-mozziconi

Reagire giochi piacevoli con il rendering lato server, non ho provato qualsiasi altro motore di rendering. react-helmet è usato per aggiungere e modificare facilmente lo <head> di ogni pagina sia lato client che lato server (ad esempio per impostare il titolo di ogni pagina). Io uso l'autoprefixer per aggiungere tutti i prefissi specifici del venditore al mio CSS/SASS, certamente non richiesto per questo esercizio.

La maggior parte del sito è quindi abbastanza semplice seguendo gli esempi nel react-router, reac-router-ssr e documentazione del casco di reazione. Vedi i documenti di quei pacchetti per i dettagli su di loro.

Prima di tutto, un file molto importante che dovrebbe trovarsi in una directory Meteor condivisa (cioè non in un server o cartella client). Questo codice configura il rendering lato server React, il tag <head>, Google Analytics, il monitoraggio di Facebook e scorre fino a ancore #hash.

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); 
    } 
} 

Ecco come sono impostati i percorsi. Notare gli attributi del titolo che vengono successivamente inseriti nel reagire-casco per impostare il contenuto <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 --il componente dell'applicazione esterna. Nota il tag <Helmet> che imposta alcuni meta tag e il titolo della pagina in base agli attributi del componente di pagina specifico.

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">... 

Una componente esempio pagina:

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> 
      ... 

Come aggiungere font differite.

Questi caratteri verranno caricati dopo il rendering iniziale e quindi non ritardano il rendering time-to-first. Credo che questo sia l'unico modo per usare i webfonts senza ridurre il punteggio di PageSpeed. Porta comunque a un breve font flash-of-wrong. Mettere questo in un file di script incluso nel client:

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); 
})(); 

Se si utilizza un servizio eccellente come fontello.com e selezionare manualmente solo le icone che realmente avete bisogno, è possibile incorporare nella vostra linea <head> CSS e ottenere le icone su prima eseguire il rendering senza attendere un file di font grande.

La Hack

che è quasi sufficiente, ma il problema è che i nostri script, CSS, e font vengono caricate in maniera sincrona e rallentare il rendering e uccidendo il nostro punteggio Page Speed. Sfortunatamente, per quanto posso dire, Meteor 1.3 non supporta ufficialmente alcun modo per allineare il CSS o aggiungere l'attributo async ai tag dello script. Dobbiamo hackerare alcune righe in 3 file del pacchetto coreplate-generator core.

~/.meteor/pacchetti/boilerplate-generatore/.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> 

Ora conta il numero di caratteri in quei 2 file modificati e inseriti i nuovi valori nel campo di lunghezza di voci quei file negli ~/.meteor/pacchetti/boilerplate-generatore/.1.0.8.4n62e6 ++ os + web.browser + web.cordova/os.json

Quindi eliminare la cartella project/.meteor/local per forzare Meteor a utilizzare il nuovo pacchetto core e riavviare l'app (la ricarica non funzionerà). Vedrai solo i cambiamenti nella modalità di produzione.

Questo è ovviamente un hack e si interromperà quando gli aggiornamenti di Meteor. Spero che pubblicando questo e ottenendo un certo interesse, lavoreremo verso un modo migliore.

da fare

cose da migliorare sarebbe:

  • Evitare l'hack.Ottenere MDG per supportare ufficialmente lo script asincrono e in linea CSS in modo flessibile
  • Lasciare un controllo granulare su cui CSS per linea e che di differire
  • consentono il controllo granulare su cui JS per asyn, e che per la sincronizzazione e cui inline .
+0

Una questione rilevante per GitHub Meteor discutere proposte di modifica al testo standard: https://github.com/meteor/meteor/pull/3860 – Noland

+0

Sto anche indagando se questo pacchetto può evitare l'hack: https://atmospherejs.com/meteorhacks/inject-initial – Noland

+0

Hey @Noland - post interessante. Puoi dirmi come gestisci i tuoi dati DDP come negli abbonamenti a react-router-ssr? – TJR