Magento – Magento 2: move the javascript to the end of the body

javascriptlayoutmagento-2.1magento2performance

As Magento 2 migrated away from Prototype I was expecting that this would be fixed in M2.

So, is there a way (easy or not) to move all the <script> tags to the end of the <body> tag instead of the <head> ?

Best Answer

Create Module Company/DeferJS
registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Company_DeferJS',
    __DIR__
);

etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Company_DeferJS" setup_version="1.0.0"/>
</config>

etc/frontend/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="controller_front_send_response_before">
        <observer name="company_deferjs" instance="Company\DeferJS\Model\Observer" shared="false" />
    </event>
</config>

etc/adminhtml/system.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="company" translate="label" sortOrder="300">
            <label>Company Extension</label>
        </tab>
        <section id="deferjs" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
            <label>Defer Javascript</label>
            <tab>company</tab>
            <resource>Company_DeferJS::config_deferjs</resource>
            <group id="general" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>General Options</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="home_page" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Exclude Home Page</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                    <comment><![CDATA[Home page will be unaffected by defer js]]></comment>
                </field>
                <field id="controller" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Exclude Controllers</label>
                    <frontend_model>Company\DeferJS\Block\System\Form\Field\Deferjs</frontend_model>
                    <backend_model>Magento\Config\Model\Config\Backend\Serialized\ArraySerialized</backend_model>
                    <comment><![CDATA[Controllers will be unaffected by defer js. Use Like: {module}_{controller}_{action}.]]></comment>
                </field>
                <field id="path" translate="label" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Exclude Paths</label>
                    <frontend_model>Company\DeferJS\Block\System\Form\Field\Deferjs</frontend_model>
                    <backend_model>Magento\Config\Model\Config\Backend\Serialized\ArraySerialized</backend_model>
                    <comment><![CDATA[Paths will be unaffected by defer js. Use Like: (Example: women/tops-women/hoodies-and-sweatshirts-women.html).]]></comment>
                </field>
            </group>
        </section>
    </system>
</config>

Model/Observer.php

<?php
namespace Company\DeferJS\Model;
use Magento\Framework\App\Http\Context;
use Magento\Framework\App\Request\Http as HttpRequest;
class Observer implements \Magento\Framework\Event\ObserverInterface {
    protected $_helper;
    public function __construct(
    \Company\DeferJS\Helper\Data $helper
    ) {
        $this->_helper = $helper;
    }
    public function execute(\Magento\Framework\Event\Observer $observer) {
        $request = $observer->getEvent()->getData('request');
        if (!$this->_helper->isEnabled($request))
            return;
        $response = $observer->getEvent()->getData('response');
        if (!$response)
            return;
        $html = $response->getBody();
        if ($html == '')
            return;
        $conditionalJsPattern = '@(?:<script type="text/javascript"|<script)(.*)</script>@msU';
        preg_match_all($conditionalJsPattern, $html, $_matches);
        $_js_if = implode('', $_matches[0]);
        $html = preg_replace($conditionalJsPattern, '', $html);
        $html = str_replace('</body>', _js_if . '</body>', $html);

        $response->setBody($html);
    }
}

Helper/Data.php

<?php
namespace Company\DeferJS\Helper;
use Magento\Framework\App\Helper\AbstractHelper;
class Data extends AbstractHelper {
    protected $_scopeConfig;
    public function __construct(
    \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
    ) {
        $this->_scopeConfig = $scopeConfig;
    }
    public function isEnabled($request) {
        $active = $this->_scopeConfig->getValue('deferjs/general/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE);
        if ($active != 1) {
            return false;
        }
        $active = $this->_scopeConfig->getValue('deferjs/general/home_page', \Magento\Store\Model\ScopeInterface::SCOPE_STORE);
        if ($active == 1 && $request->getFullActionName() == 'cms_index_index') {
            return false;
        }
        $module = $request->getModuleName();
        $controller = $request->getControllerName();
        $action = $request->getActionName();
        if ($this->regexMatchSimple($this->_scopeConfig->getValue('deferjs/general/controller', \Magento\Store\Model\ScopeInterface::SCOPE_STORE), "{$module}_{$controller}_{$action}", 1))
            return false;
        if ($this->regexMatchSimple($this->_scopeConfig->getValue('deferjs/general/path', \Magento\Store\Model\ScopeInterface::SCOPE_STORE), $request->getRequestUri(), 2))
            return false;
        return true;
    }
    public function regexMatchSimple($regex, $matchTerm, $type) {
        if (!$regex)
            return false;
        $rules = @unserialize($regex);
        if (empty($rules))
            return false;
        foreach ($rules as $rule) {
            $regex = trim($rule['defer'], '#');
            if ($regex == '')
                continue;
            if ($type == 1) {
                $regexs = explode('_', $regex);
                switch (count($regexs)) {
                    case 1:
                        $regex = $regex . '_index_index';
                        break;
                    case 2:
                        $regex = $regex . '_index';
                        break;
                    default:
                        break;
                }
            }
            $regexp = '#' . $regex . '#';
            if (@preg_match($regexp, $matchTerm))
                return true;
        }
        return false;
    }
}

Block/System/Form/Field/Deferjs.php

<?php
namespace Company\DeferJS\Block\System\Form\Field;
class Deferjs extends \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray {
    /**
     * @var \Magento\Framework\Data\Form\Element\Factory
     */
    protected $_elementFactory;
    /**
     * @param \Magento\Backend\Block\Template\Context $context
     * @param \Magento\Framework\Data\Form\Element\Factory $elementFactory
     * @param array $data
     */
    public function __construct(
    \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Data\Form\Element\Factory $elementFactory, array $data = []
    ) {
        $this->_elementFactory = $elementFactory;
        parent::__construct($context, $data);
    }
    protected function _construct() {
        $this->addColumn('defer', ['label' => __('Expression'), 'class' => 'required-entry']);
        $this->_addAfter = false;
        $this->_addButtonLabel = __('Add');
        parent::_construct();
    }
}