├── .gitignore ├── README.md ├── app ├── code │ └── community │ │ └── Pagespeed │ │ ├── Css │ │ ├── Helper │ │ │ └── Data.php │ │ ├── Model │ │ │ └── Observer.php │ │ └── etc │ │ │ ├── config.xml │ │ │ └── system.xml │ │ └── Js │ │ ├── Helper │ │ └── Data.php │ │ ├── Model │ │ └── Observer.php │ │ └── etc │ │ ├── config.xml │ │ └── system.xml └── etc │ └── modules │ ├── Pagespeed_Css.xml │ └── Pagespeed_Js.xml ├── composer.json └── modman /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Magento Google PageSpeed Optimization Extension 2 | ----------------------------------------------- 3 | 4 | This extension should help, to fulfill the requirements of the tool [Google PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/). 5 | 6 | ### Current features 7 | 8 | 1. Move all Javascript tags (head & inline) to the bottom. ```({stripped_html}{js})``` 9 | * including conditional js units ```()``` 10 | * including external js tags 11 | * including "inline" js tags 12 | 2. Move all CSS tags (head & inline) to the bottom. ```({stripped_html}{css})``` 13 | * including conditional css units ```()``` 14 | * including external css tags 15 | * including inline css tags 16 | 3. Backend configuration option to exclude specific js tags/units or css tags/units from the move. (regex pattern) 17 | 18 | ### Compatibility 19 | 20 | From Magento 1.5.x to Magento 1.9.x . 21 | 22 | ### Backend Configuration 23 | 24 | All modules (Pagespeed_Js, Pagespeed_Css) are disabled by default. 25 | 26 | Configuration path: System > Configuration > ADVANCED > Pagespeed 27 | 28 | ### How it works ? 29 | 30 | Simple parse the final html stream on the event "controller_front_send_response_before". 31 | 32 | ### What about performance/parsing time ? 33 | 34 | On our local hardware the html parsing requires a maximum of 4 milliseconds. 35 | 36 | ### Requirements from PageSpeed Insights and planned features 37 | 38 | 1. ~~[Eliminate render-blocking JavaScript and CSS in above-the-fold content](https://developers.google.com/speed/docs/insights/BlockingJS)~~ (feature 1 & 2) 39 | 2. ~~[Prioritize visible content](https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent)~~ (possible with feature 3) 40 | 41 | ### Requirements from PageSpeed Insights which are covered by 3rd party extensions 42 | 43 | 1. [Minify CSS](https://developers.google.com/speed/docs/insights/MinifyResources) 44 | 2. [Minify JavaScript](https://developers.google.com/speed/docs/insights/MinifyResources) 45 | * Both are covered by [Speedster Advanced by Fooman](http://www.magentocommerce.com/magento-connect/speedster-advanced-by-fooman.html) (note: that we have no experience with this extension, but Fooman seems to be a good guy.) 46 | 3. [Optimize images](https://developers.google.com/speed/docs/insights/OptimizeImages) 47 | * [Image Optimization](http://www.magentocommerce.com/magento-connect/image-optimization.html)(note: no experience too.) 48 | 4. [Minify HTML](https://developers.google.com/speed/docs/insights/MinifyResources) 49 | * Based primary on [Minify CSS](https://developers.google.com/speed/docs/insights/MinifyResources) and [Minify JavaScript](https://developers.google.com/speed/docs/insights/MinifyResources). 50 | 51 | ### Requirements from PageSpeed Insights which are covered by your server admin :) 52 | 53 | 1. [Enable compression](https://developers.google.com/speed/docs/insights/EnableCompression) 54 | 2. [Avoid landing page redirects](https://developers.google.com/speed/docs/insights/AvoidRedirects) 55 | 3. [Leverage browser caching](https://developers.google.com/speed/docs/insights/LeverageBrowserCaching) 56 | 4. [Reduce server response time](https://developers.google.com/speed/docs/insights/Server) 57 | 58 | ### Goal 59 | 60 | ![Goal](http://www.mediarox.de/goal.png) 61 | 62 | ### Notices 63 | 64 | 1. There is also a great tool called [PageSpeed Module](https://developers.google.com/speed/pagespeed/module) 65 | for common webservers like apache and nginx. If you have the opportunity: Use it, but read the manual. 66 | 2. Test before use. There are also "great" things like multiple `````` tags, that will crash the party. 67 | 3. Front Page Cache: Test it. Look that our event "controller_front_send_response_before" is called before 68 | your FPC-Extension starts to observe. 69 | 4. If an Javascript use the outdated "document.write", it must be excluded by the regex pattern. 70 | 71 | ### Developers 72 | 73 | * Steven Fritzsche [@de_mediarox](https://twitter.com/de_mediarox) 74 | * Thomas Uhlig [Xing](https://www.xing.com/profile/Thomas_Uhlig24) 75 | 76 | ### Special thanks 77 | 78 | * Sander Kwantes [sanderkwantes](https://github.com/sanderkwantes) 79 | * Adam Johnson [adamj88] (https://github.com/adamj88) 80 | * Ben Corlett [bencorlett] (https://github.com/bencorlett) 81 | 82 | ### Features inspired by 83 | 84 | * Daniel Chicote [Github](https://github.com/danielchicote) 85 | * Henk Valk [Github](https://github.com/henkvalk) 86 | * Dan Stevens [from Activ8](https://twitter.com/Activ8Ltd) 87 | 88 | ### Licence 89 | 90 | [OSL - Open Software Licence 3.0](http://opensource.org/licenses/osl-3.0.php) 91 | 92 | ### Copyright 93 | 94 | (c) 2015 mediarox UG (haftungsbeschraenkt) (http://www.mediarox.de) 95 | -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Css/Helper/Data.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Thomas Uhlig 7 | */ 8 | 9 | /** 10 | * Standard helper 11 | */ 12 | class Pagespeed_Css_Helper_Data extends Mage_Core_Helper_Abstract 13 | { 14 | /** 15 | * Configuration paths 16 | */ 17 | const PAGESPEED_CSS_ENABLED = 'pagespeed/css/enabled'; 18 | const PAGESPEED_CSS_EXCLUDE_ENABLED = 'pagespeed/css/exclude_enabled'; 19 | const PAGESPEED_CSS_EXCLUDE = 'pagespeed/css/exclude'; 20 | 21 | /** 22 | * Is css module enabled ? 23 | * 24 | * @return bool 25 | */ 26 | public function isEnabled() 27 | { 28 | return Mage::getStoreConfigFlag(self::PAGESPEED_CSS_ENABLED); 29 | } 30 | 31 | /** 32 | * Is exclude list enabled ? 33 | * 34 | * @return bool 35 | */ 36 | public function isExcludeEnabled() 37 | { 38 | return Mage::getStoreConfigFlag(self::PAGESPEED_CSS_EXCLUDE_ENABLED); 39 | } 40 | 41 | /** 42 | * Retrieve css configuration exclude list 43 | * 44 | * @return array of regex patterns 45 | */ 46 | public function getExcludeList() 47 | { 48 | $result = array(); 49 | if ($this->isExcludeEnabled()) { 50 | $exclude = Mage::getStoreConfig(self::PAGESPEED_CSS_EXCLUDE); 51 | $exclude = explode(PHP_EOL, $exclude); 52 | foreach ($exclude as $item) { 53 | if ($item = trim($item)) { 54 | $result[] = $item; 55 | } 56 | } 57 | } 58 | return $result; 59 | } 60 | } -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Css/Model/Observer.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Thomas Uhlig 7 | */ 8 | 9 | /** 10 | * Standard observer class 11 | */ 12 | class Pagespeed_Css_Model_Observer 13 | { 14 | /** 15 | * @const string 16 | */ 17 | const HTML_TAG_BODY = ''; 18 | 19 | /** 20 | * Will finally contain all css tags to move. 21 | * @var string 22 | */ 23 | private $cssTags = ''; 24 | 25 | /** 26 | * Contains all exclude regex patterns. 27 | * @var array 28 | */ 29 | private $excludeList = array(); 30 | 31 | /** 32 | * Processes the matched single css tag or the conditional css tag group. 33 | * 34 | * Step 1: Return if hit is blacklisted by exclude list. 35 | * Step 2: Add hit to css tag list and return empty string for the replacement. 36 | * 37 | * @param array $hits 38 | * @return string 39 | */ 40 | public function processHit($hits) 41 | { 42 | // Step 1 43 | if ($this->isHitExcluded($hits[0])) return $hits[0]; 44 | 45 | // Step 2 46 | $this->cssTags .= $hits[0]; 47 | return ''; 48 | } 49 | 50 | /** 51 | * Is hit on exclude list? 52 | * 53 | * @param string $hit 54 | * @return bool 55 | */ 56 | protected function isHitExcluded($hit) 57 | { 58 | $c = 0; 59 | preg_replace($this->excludeList, '', $hit, -1, $c); 60 | return ($c > 0); 61 | } 62 | 63 | /** 64 | * Move Css (head & inline) to the bottom. ({excluded_css}{stripped_html}{css}) 65 | * 66 | * Step 1: Return if module is disabled. 67 | * Step 2: Load needed data. 68 | * Step 3: Return if no is found in html. 69 | * Step 4: Search and replace conditional css units. (example: ) 70 | * Step 5: Search and replace external css tags. (link-tags must xhtml-compliant closed by "/>") 71 | * Step 6: Search and replace inline css tags. 72 | * Step 7: Return if no css is found. 73 | * Step 8: Remove blank lines from html. 74 | * Step 9: Recalculating position, insert css groups right before body ends and set response. 75 | * Final order: 76 | * 1. excluded css 77 | * 2. stripped html 78 | * 3. conditional css tags 79 | * 4. external css tags 80 | * 5. inline css tags 81 | * 6. 82 | * 83 | * @param Varien_Event_Observer $observer 84 | */ 85 | public function parseCssToBottom(Varien_Event_Observer $observer) 86 | { 87 | //$timeStart = microtime(true); 88 | 89 | // Step 1 90 | $helper = Mage::helper('pagespeed_css'); 91 | if (!$helper->isEnabled()) return; 92 | 93 | // Step 2 94 | $response = $observer->getFront()->getResponse(); 95 | $html = $response->getBody(); 96 | $this->excludeList = $helper->getExcludeList(); 97 | 98 | // Step 3 99 | $closedBodyPosition = strripos($html, self::HTML_TAG_BODY); 100 | if (false === $closedBodyPosition) return; 101 | 102 | // Step 4 103 | $html = preg_replace_callback( 104 | '#\<\!--\[if[^\>]*>\s*]*type\="text\/css"[^>]*/>\s*<\!\[endif\]-->#isU', 105 | 'self::processHit', 106 | $html 107 | ); 108 | 109 | // Step 5 110 | $html = preg_replace_callback( 111 | '#]*type\=["\']text\/css["\'][^>]*/>#isU', 112 | 'self::processHit', 113 | $html 114 | ); 115 | 116 | // Step 6 117 | $html = preg_replace_callback( 118 | '##isUm', 119 | 'self::processHit', 120 | $html 121 | ); 122 | 123 | // Step 7 124 | if (!$this->cssTags) return; 125 | 126 | // Step 8 127 | $html = preg_replace('/^\h*\v+/m', '', $html); 128 | 129 | // Step 9 130 | $closedBodyPosition = strripos($html, self::HTML_TAG_BODY); 131 | $html = substr_replace($html, $this->cssTags, $closedBodyPosition, 0); 132 | $response->setBody($html); 133 | 134 | //Mage::log(round(((microtime(true) - $timeStart) * 1000)) . ' ms taken to parse Css to bottom'); 135 | } 136 | } -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Css/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 0.0.0.2 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | singleton 27 | pagespeed_css/observer 28 | parseCssToBottom 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | Pagespeed_Css_Model 42 | 43 | 44 | 45 | 46 | Pagespeed_Css_Helper 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Pagespeed 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 0 83 | 1 84 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Css/etc/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | advanced 15 | text 16 | 990 17 | 1 18 | 1 19 | 1 20 | 21 | 22 | 23 | text 24 | 20 25 | 1 26 | 1 27 | 1 28 | 1 29 | 30 | 31 | 32 | select 33 | adminhtml/system_config_source_yesno 34 | 10 35 | 1 36 | 1 37 | 1 38 | 39 | 40 | 41 | select 42 | adminhtml/system_config_source_yesno 43 | 20 44 | 1 45 | 1 46 | 1 47 | 48 | 1 49 | 50 | 51 | 52 | 53 | textarea 54 | 30 55 | 1 56 | 1 57 | 1 58 | Regular expressions (one per line including delimiters) 59 | 60 | 1 61 | 1 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Js/Helper/Data.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Thomas Uhlig 7 | */ 8 | 9 | /** 10 | * Standard helper 11 | */ 12 | class Pagespeed_Js_Helper_Data extends Mage_Core_Helper_Abstract 13 | { 14 | /** 15 | * Configuration paths 16 | */ 17 | const PAGESPEED_JS_ENABLED = 'pagespeed/js/enabled'; 18 | const PAGESPEED_JS_EXCLUDE_ENABLED = 'pagespeed/js/exclude_enabled'; 19 | const PAGESPEED_JS_EXCLUDE = 'pagespeed/js/exclude'; 20 | 21 | /** 22 | * Is js module enabled ? 23 | * 24 | * @return bool 25 | */ 26 | public function isEnabled() 27 | { 28 | return Mage::getStoreConfigFlag(self::PAGESPEED_JS_ENABLED); 29 | } 30 | 31 | /** 32 | * Is exclude list enabled ? 33 | * 34 | * @return bool 35 | */ 36 | public function isExcludeEnabled() 37 | { 38 | return Mage::getStoreConfigFlag(self::PAGESPEED_JS_EXCLUDE_ENABLED); 39 | } 40 | 41 | /** 42 | * Retrieve js configuration exclude list 43 | * 44 | * @return array of regex patterns 45 | */ 46 | public function getExcludeList() 47 | { 48 | $result = array(); 49 | if ($this->isExcludeEnabled()) { 50 | $exclude = Mage::getStoreConfig(self::PAGESPEED_JS_EXCLUDE); 51 | $exclude = explode(PHP_EOL, $exclude); 52 | foreach ($exclude as $item) { 53 | if ($item = trim($item)) { 54 | $result[] = $item; 55 | } 56 | } 57 | } 58 | return $result; 59 | } 60 | } -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Js/Model/Observer.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Thomas Uhlig 7 | */ 8 | 9 | /** 10 | * Standard observer class 11 | */ 12 | class Pagespeed_Js_Model_Observer 13 | { 14 | /** 15 | * @const string 16 | */ 17 | const HTML_TAG_BODY = ''; 18 | 19 | /** 20 | * Will finally contain all js tags to move. 21 | * @var string 22 | */ 23 | private $jsTags = ''; 24 | 25 | /** 26 | * Contains all exclude regex patterns. 27 | * @var array 28 | */ 29 | private $excludeList = array(); 30 | 31 | /** 32 | * Processes the matched single js tag or the conditional js tag group. 33 | * 34 | * Step 1: Return if hit is blacklisted by exclude list. 35 | * Step 2: Add hit to js tag list and return empty string for the replacement. 36 | * 37 | * @param array $hits 38 | * @return string 39 | */ 40 | public function processHit($hits) 41 | { 42 | // Step 1 43 | if ($this->isHitExcluded($hits[0])) return $hits[0]; 44 | 45 | // Step 2 46 | $this->jsTags .= $hits[0]; 47 | return ''; 48 | } 49 | 50 | /** 51 | * Is hit on exclude list? 52 | * 53 | * @param string $hit 54 | * @return bool 55 | */ 56 | protected function isHitExcluded($hit) 57 | { 58 | $c = 0; 59 | preg_replace($this->excludeList, '', $hit, -1, $c); 60 | return ($c > 0); 61 | } 62 | 63 | /** 64 | * Move Js (head & inline) to the bottom. ({excluded_js}{stripped_html}{js}) 65 | * 66 | * Step 1: Return if module is disabled. 67 | * Step 2: Load needed data. 68 | * Step 3: Return if no is found in html. 69 | * Step 4: Search and replace conditional js units. (example: ) 70 | * Step 5: Search and replace normal js tags. 71 | * Step 6: Return if no js is found. 72 | * Step 7: Remove blank lines from html. 73 | * Step 8: Recalculating position, insert js groups right before body ends and set response. 74 | * Final order: 75 | * 1. excluded js 76 | * 2. stripped html 77 | * 3. conditional js tags 78 | * 4. normal js tags 79 | * 5. 80 | * 81 | * @param Varien_Event_Observer $observer 82 | */ 83 | public function parseJsToBottom(Varien_Event_Observer $observer) 84 | { 85 | //$timeStart = microtime(true); 86 | 87 | // Step 1 88 | $helper = Mage::helper('pagespeed_js'); 89 | if (!$helper->isEnabled()) return; 90 | 91 | // Step 2 92 | $response = $observer->getFront()->getResponse(); 93 | $html = $response->getBody(); 94 | $this->excludeList = $helper->getExcludeList(); 95 | 96 | // Step 3 97 | $closedBodyPosition = strripos($html, self::HTML_TAG_BODY); 98 | if (false === $closedBodyPosition) return; 99 | 100 | // Step 4 101 | $html = preg_replace_callback( 102 | '#\<\!--\[if[^\>]*>\s*\s*<\!\[endif\]-->#isU', 103 | 'self::processHit', 104 | $html 105 | ); 106 | 107 | // Step 5 108 | $html = preg_replace_callback( 109 | '##isU', 110 | 'self::processHit', 111 | $html 112 | ); 113 | 114 | // Step 6 115 | if (!$this->jsTags) return; 116 | 117 | // Step 7 118 | $html = preg_replace('/^\h*\v+/m', '', $html); 119 | 120 | // Step 8 121 | $closedBodyPosition = strripos($html, self::HTML_TAG_BODY); 122 | $html = substr_replace($html, $this->jsTags, $closedBodyPosition, 0); 123 | $response->setBody($html); 124 | 125 | //Mage::log(round(((microtime(true) - $timeStart) * 1000)) . ' ms taken to parse Js to bottom'); 126 | } 127 | } -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Js/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 0.0.0.2 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | singleton 27 | pagespeed_js/observer 28 | parseJsToBottom 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | Pagespeed_Js_Model 42 | 43 | 44 | 45 | 46 | Pagespeed_Js_Helper 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Pagespeed 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 0 83 | 1 84 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/code/community/Pagespeed/Js/etc/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | advanced 15 | text 16 | 990 17 | 1 18 | 1 19 | 1 20 | 21 | 22 | 23 | text 24 | 10 25 | 1 26 | 1 27 | 1 28 | 1 29 | 30 | 31 | 32 | select 33 | adminhtml/system_config_source_yesno 34 | 10 35 | 1 36 | 1 37 | 1 38 | 39 | 40 | 41 | select 42 | adminhtml/system_config_source_yesno 43 | 20 44 | 1 45 | 1 46 | 1 47 | 48 | 1 49 | 50 | 51 | 52 | 53 | textarea 54 | 30 55 | 1 56 | 1 57 | 1 58 | Regular expressions (one per line including delimiters) 59 | 60 | 1 61 | 1 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/etc/modules/Pagespeed_Css.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | true 14 | community 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/etc/modules/Pagespeed_Js.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | true 14 | community 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediarox/pagespeed", 3 | "type": "magento-module", 4 | "require": { 5 | "magento-hackathon/magento-composer-installer": "*" 6 | } 7 | } -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | # modman file for pagespeed 2 | app/code/community/Pagespeed/Css/ app/code/community/Pagespeed/Css/ 3 | app/code/community/Pagespeed/Js/ app/code/community/Pagespeed/Js/ 4 | app/etc/modules/Pagespeed_Css.xml app/etc/modules/Pagespeed_Css.xml 5 | app/etc/modules/Pagespeed_Js.xml app/etc/modules/Pagespeed_Js.xml 6 | --------------------------------------------------------------------------------