5

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

11

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 .= $_js_if;
        $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();
    }
}
  • This works very well. Only oddness, on product page, Google says the .js is not deferred, but viewing source shows code is deferred. Thank you very much, has reduced load to 1.8s according to gmetrix and a 95 rating – dawhoo Oct 28 '17 at 1:31
  • I'm getting an error of "Undefined index: id in /var/www/html/vendor/magento/module-config/Model/Config/Structure/Element/Iterator.php on line 59" when attempting this solution. I think it has something to do with the tab ID's in system.xml, but am not 100% sure. Any suggestions? – codestr Feb 26 '18 at 22:21
  • This code working perfectly could you please share your issue in more details. – Pratik Feb 27 '18 at 5:35
3

Short answer: there is a way but it will require a huge amount of work. Magento has a lot of inline javascript which requires (at least some) external javascript files to be loaded (mainly requirejs).

Currently only the bare minimum is loaded in the head section: requirejs/require.js, requirejs/mixins.js and requirejs-config.js.

However, Magento encourages developers to use the default_head_blocks.xml file to include custom script and style files if not using requirejs. Most 3rd party extension builders will use this.

Therefore, rewriting all output to move javascript to the end of the body section will require a lot of effort, and will have very low maintainability (you'll have to re-check all your work for every version upgrade or 3rd party extension you install).

1

It may be complicated to move all the tags to the end of the tag instead of the manually.

There is a way to do this thing called deferring JavaScript files to the end of page so that these JS files can be loaded after all HTML and CSS. We can use a third party extension to make it simpler than using manual ways:

https://marketplace.magento.com/bsscommerce-defer-js.html

By this way, JavaScript is deferred till the last moment and when the browser renders a page, it will start with HTML and CSS before JavaScript which can reduce loading time and even enhance site performance.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.