myCSharp.de - DIE C# und .NET Community
Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 
 | Suche | FAQ

» Hauptmenü
myCSharp.de
» Startseite
» Forum
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Suche
» Regeln
» Wie poste ich richtig?
» Forum-FAQ

Mitglieder
» Liste / Suche
» Wer ist wo online?

Ressourcen
» openbook: Visual C#
» openbook: OO
» Microsoft Docs

Team
» Kontakt
» Übersicht
» Wir über uns

» myCSharp.de Diskussionsforum
Du befindest Dich hier: Community-Index » Diskussionsforum » Gemeinschaft » .NET-Komponenten und C#-Snippets » HTML Minification mit ASP.NET MVC
Letzter Beitrag | Erster ungelesener Beitrag Druckvorschau | Thema zu Favoriten hinzufügen

Antwort erstellen
Zum Ende der Seite springen  

HTML Minification mit ASP.NET MVC

 
Autor
Beitrag « Vorheriges Thema | Nächstes Thema »
Abt
myCSharp.de-Team

avatar-4119.png


Dabei seit: 20.07.2008
Beiträge: 14.488
Herkunft: BW


Abt ist offline

HTML Minification mit ASP.NET MVC

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Beschreibung:

Angetrieben von der Tatsache, dass das zu übertragende Volumen einer Webseite direkte Auswirkungen auf die Reaktionsgeschwindigkeit hat, habe ich mich am Wochenende an den Schreibtisch gesetzt, um einen  ActionFilter für ASP.NET MVC zu schreiben, der unnötige Elemente aus dem HTML-Code, das an den Client gesendet werden soll, entfernt.

Dafür habe ich ein entsprechendes Snippet geschrieben

C#-Code:
namespace HtmlMinifyExample
{
    /// <summary>
    /// Minifies the HTML code. Removes all whitespaces
    /// </summary>
    public class HtmlMinifyStream : MemoryStream
    {
        #region Properties and Fields
        /// <summary>
        /// Regex; Pattern by http://stackoverflow.com/questions/8762993/remove-white-space-from-entire-html-but-inside-pre-with-regular-expressions
        /// </summary>
        private static readonly Regex RegExpressionWhiteSpaces = new Regex( @"(?<=\s)\s+(?![^<>]*</pre>)", RegexOptions.Compiled );
        // Regex instances are read-only => Thread-safe and can be static

        /// <summary>
        /// Stream to minify.
        /// Do not dispose by yourself!
        /// </summary>
        private readonly Stream _responseStream;
        #endregion

        #region Constructor
        /// <summary>
        /// Create new minify stream based on the current response stream. Overwrites contents
        /// </summary>
        public HtmlMinifyStream( Stream responseStream )
        {
            this._responseStream = responseStream;
        }
        #endregion

        /// <summary>
        /// Override close method and implement minification
        /// </summary>
        public override void Close()
        {
            // Minify
            var minified = Encoding.UTF8.GetBytes( RegExpressionWhiteSpaces.Replace( Encoding.UTF8.GetString( this.ToArray( ) ), String.Empty ) );

            // write data to stream
            _responseStream.Write( minified, 0, minified.Length );
            // flush all
            _responseStream.Flush( );


            base.Close( );
        }
    }
}

Dieses kann sowohl als Filter für die gesamte ASP.NET MVC Anwendung verwendet werden

C#-Code:
public class HtmlMinimyFilter : IActionFilter
{
    public void OnActionExecuting( ActionExecutingContext filterContext )
    {
        // We dont care
    }

    /// <summary>
    /// Minification at the end of the request
    /// </summary>
    public void OnActionExecuted( ActionExecutedContext filterContext )
    {
        if ( IsSupportedContentType( filterContext ) )
        {
            filterContext.HttpContext.Response.Filter = new HtmlMinifyStream( filterContext.HttpContext.Response.Filter );
        }
    }

    /// <summary>
    /// Returns true if filter is not null and content type is html.
    /// </summary>
    private Boolean IsSupportedContentType( ActionExecutedContext filterContext )
    {
        return filterContext.HttpContext.Response.Filter != null && filterContext.HttpContext.Response.ContentType.Equals( "text/html", StringComparison.OrdinalIgnoreCase );
    }
}

oder als Attribut für ausgewählte Actions

C#-Code:
public class HtmlMinifyAttribute : ActionFilterAttribute
{
    /// <summary>
    /// Implement attribute behavior
    /// </summary>
    public override void OnActionExecuted( ActionExecutedContext filterContext )
    {
        if ( IsSupportedContentType( filterContext ) )
        {
            filterContext.HttpContext.Response.Filter = new HtmlMinifyStream( filterContext.HttpContext.Response.Filter );
        }
    }

    /// <summary>
    /// Returns troe if filter is not null and content type is html.
    /// </summary>
    private Boolean IsSupportedContentType( ActionExecutedContext filterContext )
    {
        return filterContext.HttpContext.Response.Filter != null && filterContext.HttpContext.Response.ContentType.Equals( "text/html", StringComparison.OrdinalIgnoreCase );
    }
}

Einsparpotential gängier Webseiten: ca. 10- 30%.
Performance-Verlust vernachlässigbar

Falls weitere Erklärung bzgl. Messung und Umsetzung notwendig: ist auch in meinem Blog unter  HtmlMinifier für ASP.NET MVC zu lesen.

Den Quellcode der gesamten Testanwendung habe ich auf  GitHub bereit gestellt.

Schlagwörter: ASP.NET, Minification, Html, Performance, Compression
07.07.2014 09:55 Beiträge des Benutzers | zu Buddylist hinzufügen
Sarc
myCSharp.de-Mitglied

Dabei seit: 29.09.2008
Beiträge: 417
Entwicklungsumgebung: VS 2012


Sarc ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Hallo Abt,

dazu habe ich gleich mehrere Anmerkungen:
  1. Pattern nur bedingt korrekt: Das Regex pattern matched nur aufeinanderfolgende Whitespaces und lässt das erste stehen, d.h. es werden z.b. nicht alle Zeilenumbrüche entfernt wie angegeben, sondern aus \r\n wird \r. Das lässt sich noch optimieren.
  2. Problem mit <pre> Tags: Dein Regex-Pattern versucht zwar diese Problem zu lösen, allerdings wäre das so nur korrekt, wenn du dein Regex auf den kompletten HTML-String anwendest. Du arbeitest in der Write-Methode aber Block-basiert, d.h. ein </pre>-Tag könnte erst im nächsten Block enden, wodurch du fälschlicherweise die Whitespaces entfernen würdest.
  3. Performance: Die von dir genutzte Performance-Messung macht keinen Sinn, da die OnActionExecuted-Methode deines Filters (und damit das Ende der Messung) aufgerufen wird, bevor die eigentliche Minification stattfindet. Wenn du misst wie lange das eigentliche Minifizieren dauert, wirst du sehen, dass es einen wahnsinnig hohen Performance Impact hat. Ich habe eine kurze Messung gemacht, mit und ohne Minifikation (jeweils 100 Durchläufe). Das Ergebnis ist als Boxplot unten beigefügt.

Sarc hat dieses Bild (verkleinerte Version) angehängt:
boxplot.png
Volle Bildgröße

07.07.2014 12:46 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Abt
myCSharp.de-Team

avatar-4119.png


Dabei seit: 20.07.2008
Beiträge: 14.488
Herkunft: BW

Themenstarter Thema begonnen von Abt

Abt ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Hi, danke für Deine Rückmeldung.
Das mit der Performance kann ich so nicht nachvollziehen. Du siehst, dass ich nicht nur die Ticks beachtet habe, sondern auch die Gesamtzeit der Auslieferung.

Warum hab ich das?
Normalerweise arbeiten wir im Web ja mittlerweile mit GZipStreams.
Sprich ich liefer weniger aus, und ich muss weniger komprimieren. Dadurch komm ich bei 1000 Durchläufen auf eine totale Differenz von 2ms.
Ich hab das lokal wie auch auf Azure getestet und konnte hier in der Relation gleiche Ergebnis erreichen.
Laut Debug greift mein PerformanceFilter NACH dem Minify und misst somit korrekt.

Ich arbeite auch nicht Block-basierend sondern durchforste den gesamten Inhalt auf einmal.
07.07.2014 12:52 Beiträge des Benutzers | zu Buddylist hinzufügen
Sarc
myCSharp.de-Mitglied

Dabei seit: 29.09.2008
Beiträge: 417
Entwicklungsumgebung: VS 2012


Sarc ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Also ich hab mir aus Github den Master runtergeladen und da kommt das Minify definitiv danach. Ich hab zum Spaß einfach mal ein Thread.Sleep(100) (nach dem Minify) eingefügt und die Ausgabe auf der Webseite war weiterhin unter 100 Ticks. Oder verwendest du eine andere Version?

Bzgl. dem Block-basiert: Dein Quellcode zeigt doch, dass du in der Write-Methode mit dem übergebenen Buffer und damit nicht mit dem gesamten HTML auf einmal arbeitest. Du wirst beim Debuggen sicher sehen, dass mehrmals in die Write-Methode gesprungen wird.

Übrigens kann ich mir das mit den 2ms nicht wirklich vorstellen. Mein Rechner benötigt für folgendes schon länger als 3ms:

C#-Code:
private static readonly Regex RegExpressionWhiteSpaces = new Regex(@"(?<=\s)\s+(?![^<>]*</pre>)", RegexOptions.Compiled);

private const string Test = "lksdhf sdkhf  shkdfahöksdklf slfhkl ahklfahklfhl ksafdhk fafdhkl fldhk fldhk sdhklfhkl afhkl fdhkl afhkl fhkl ahklf hk sdakfh sdafh lsd aflk sdlfs ";

static void Main(string[] args)
{
    var sw = Stopwatch.StartNew();
    RegExpressionWhiteSpaces.Replace(Test, string.Empty);
    sw.Stop();

    Console.WriteLine(sw.Elapsed.TotalMilliseconds);
    Console.Read();
}

Dieser Beitrag wurde 2 mal editiert, zum letzten Mal von Sarc am 07.07.2014 13:07.

07.07.2014 13:03 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Abt
myCSharp.de-Team

avatar-4119.png


Dabei seit: 20.07.2008
Beiträge: 14.488
Herkunft: BW

Themenstarter Thema begonnen von Abt

Abt ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Das braucht 3 ms, weil 2.9 ms dafür das Erstellen des Regex braucht.... Achtung Der wird ja erst beim ersten Zugriff erstellt. Du misst aber immer nur den ersten Zugriff.
Bei mir braucht auch der erste Request > 300ms und 80.000 Ticks. Jeder weitere aber nicht.

Korrekt wäre:

C#-Code:
    class Program
    {
        private static readonly Regex RegExpressionWhiteSpaces = new Regex( @"(?<=\s)\s+(?![^<>]*</pre>)", RegexOptions.Compiled );

        private const string Test = "lksdhf sdkhf  shkdfahöksdklf slfhkl ahklfahklfhl ksafdhk fafdhkl fldhk fldhk sdhklfhkl afhkl fdhkl afhkl fhkl ahklf hk sdakfh sdafh lsd aflk sdlfs ";

        static void Main( string[ ] args )
        {
            // First call -> compile
            var fc = Stopwatch.StartNew( );
            RegExpressionWhiteSpaces.Replace( Test, string.Empty );
            fc.Stop( );
            var sw = Stopwatch.StartNew( );
            for ( var i = 0 ; i < 1000 ; i++ )
            {
                RegExpressionWhiteSpaces.Replace( Test, string.Empty );
            }
            sw.Stop( );

            Console.WriteLine( "First Call: " + fc.Elapsed.TotalMilliseconds + " Total: " + sw.Elapsed.TotalMilliseconds + " - per Call: " + ( sw.Elapsed.TotalMilliseconds / 1000 ) );
            Console.Read( );
        }
    }

Komm ich auf den Output:
First Call: 1,8968 Total: 8,5726 - per Call: 0,0085726
Ist bei mir "deutlich" unter 3ms - Debug Modus - und da ist sogar der Schleifen-Overhead enthalten Augenzwinkern

Zitat:
Du wirst beim Debuggen sicher sehen, dass mehrmals in die Write-Methode gesprungen wird.

Ja das seh ich; aber erst ab einer ziemlich großen Seite und nicht mit dem Testcode. Da werd ich mir evtl. noch eine Lösung einfallen lassen.
07.07.2014 13:48 Beiträge des Benutzers | zu Buddylist hinzufügen
Abt
myCSharp.de-Team

avatar-4119.png


Dabei seit: 20.07.2008
Beiträge: 14.488
Herkunft: BW

Themenstarter Thema begonnen von Abt

Abt ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Danke für den Einwand mit der Blockverarbeitung ab einer gewissen Seitengröße
Hier das Update:

C#-Code:
namespace HtmlMinifyExample
{
    /// <summary>
    /// Minifies the HTML code. Removes all whitespaces
    /// </summary>
    public class HtmlMinifyStream : MemoryStream
    {
        #region Properties and Fields
        /// <summary>
        /// Regex; Pattern by http://stackoverflow.com/questions/8762993/remove-white-space-from-entire-html-but-inside-pre-with-regular-expressions
        /// </summary>
        private static readonly Regex RegExpressionWhiteSpaces = new Regex( @"(?<=\s)\s+(?![^<>]*</pre>)", RegexOptions.Compiled );
        // Regex instances are read-only => Thread-safe and can be static

        /// <summary>
        /// Stream to minify.
        /// Do not dispose by yourself!
        /// </summary>
        private readonly Stream _responseStream;
        #endregion

        #region Constructor
        /// <summary>
        /// Create new minify stream based on the current response stream. Overwrites contents
        /// </summary>
        public HtmlMinifyStream( Stream responseStream )
        {
            this._responseStream = responseStream;
        }
        #endregion

        /// <summary>
        /// Override close method and implement minification
        /// </summary>
        public override void Close()
        {
            // Minify
            var minified = Encoding.UTF8.GetBytes( RegExpressionWhiteSpaces.Replace( Encoding.UTF8.GetString( this.ToArray( ) ), String.Empty ) );

            // write data to stream
            _responseStream.Write( minified, 0, minified.Length );
            // flush all
            _responseStream.Flush( );


            base.Close( );
        }
    }
}

Es wird nur noch ein einziges Mal minified.
07.07.2014 14:49 Beiträge des Benutzers | zu Buddylist hinzufügen
Sarc
myCSharp.de-Mitglied

Dabei seit: 29.09.2008
Beiträge: 417
Entwicklungsumgebung: VS 2012


Sarc ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Zitat von Abt:
Das braucht 3 ms, weil 2.9 ms dafür das Erstellen des Regex braucht.... :] Der wird ja erst beim ersten Zugriff erstellt. Du misst aber immer nur den ersten Zugriff.

Da hast du Recht, das Erstellen hab ich nicht berücksichtigt. Na gut, war ein schlechtes Beispiel um dich zu überzeugen. Ich versuch es nochmal :D

Folgender Quellcode verwendet einen String, der wohl realistischer für das Szenario ist als aus meinem obigen Beispiel. In dem Fall wären das 24000 Zeichen, also keine unübliche Größe für ein generiertes HTML, sondern eher noch am unteren Limit. Es erfolgt weiterhin der erste call um das Regex zu kompilieren, danach wird mit dem echten String gemessen.
Ergebnis auf meinem Rechner: Über 300 ms für einen Durchlauf.

C#-Code:
private static readonly Regex RegExpressionWhiteSpaces = new Regex(@"(?<=\s)\s+(?![^<>]*</pre>)", RegexOptions.Compiled);
private const string Test = "lksdhf sdkhf  shkdfa\r\nhöksdklf slfhkl ahklfahklfhl ksa    fdhk fafdhkl fldhk fldhk sdhklfhkl afhkl     fdhkl \r\n afhkl fhkl ahklf hk sdakfh sdafh lsd aflk sdlfs ";

static void Main(string[] args)
{
    // First call -> compile
    var fc = Stopwatch.StartNew();
    RegExpressionWhiteSpaces.Replace(Test, string.Empty);
    fc.Stop();

    var sb = new StringBuilder();
    for (int i = 0; i < 150; ++i)
        sb.Append(Test);

    var s = sb.ToString();
    // Länge des erzeugten strings = 24000
    Console.WriteLine("String length: " + s.Length);

    var sw = Stopwatch.StartNew();
    RegExpressionWhiteSpaces.Replace(s, string.Empty);
    sw.Stop();

    Console.WriteLine("First Call: " + fc.Elapsed.TotalMilliseconds + " Second call: " + sw.Elapsed.TotalMilliseconds);
    Console.Read();
}
07.07.2014 15:44 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Abt
myCSharp.de-Team

avatar-4119.png


Dabei seit: 20.07.2008
Beiträge: 14.488
Herkunft: BW

Themenstarter Thema begonnen von Abt

Abt ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Ich hab mal die aktuelle Seite von heise.de verwendet, das sind 571109 Zeichen laut Deinem Code - also mehr als das 20-Fache Deines Tests:
Resultat siehe Anhang.

Du musst aber eins Bedenken: HtmlMinifcation ist die Optimierung auf der "letzten Meile" und ist Bestandteil des Cachings.
Wenn Du ständig Seiten mit einer halben Million Zeichen komprimierst, dann solltest Du Dir eher Überlegungen über das Gesamtkonzept machen.
"Realistisch" ist dieser Test also gewiss nicht.

In modernen Webseiten wird der Rahmen ohne dynamischen Daten nur noch aus dem Cache geliefert. Hier spielt Minification eine Rolle, weil der komprimierte, statische Inhalt anschließend nur noch gepusht wird - und genau hier ist es interessant.
Die Daten werden dann dynamisch zB wie AngularJS und Co nachgeladen.

Statistik auf Azure - Vorher und Nachher
Lieferung ohne Minify aus dem Cache: 51ms ( Info)
Ohne Minify Filter: 93ms ( Info)
Mit Minify Filter: 104ms ( Info)
Lieferung mit Minify aus dem Cache: 40ms ( Info)

Das Auslieferungsvolumen hat sich um 27% verringert - das Lieferverhalten aus dem Cache um 20% verbessert.
Hast noch weitere Gegenargumente? :)

Abt hat dieses Bild (verkleinerte Version) angehängt:
minifyperf.png
Volle Bildgröße

07.07.2014 15:56 Beiträge des Benutzers | zu Buddylist hinzufügen
malignate
myCSharp.de-Mitglied

avatar-3206.png


Dabei seit: 18.02.2005
Beiträge: 742


malignate ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Nice, ich finds gut.

Habe zwei Wünsche:

1. Mach ein NuGet Packet draus
2. Packe noch eine globale Einstellung dazu, damit ich es es im Debug-Modus etc. global deaktivieren kann. Statisches Property oder AppSetting würde mir reichen.

Btw: Dein PerformanceMessureFilter ist nicht für parallele Requests geeignet ;)
07.07.2014 21:49 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Baumstruktur | Brettstruktur       | Top 
myCSharp.de | Forum Der Startbeitrag ist älter als 6 Jahre.
Der letzte Beitrag ist älter als 6 Jahre.
Antwort erstellen


© Copyright 2003-2021 myCSharp.de-Team | Impressum | Datenschutz | Alle Rechte vorbehalten. | Dieses Portal verwendet zum korrekten Betrieb Cookies. 24.01.2021 16:58