9

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

3 Answers 3

15

First create new module:

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();
    }
}
3
  • 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
    Commented Oct 28, 2017 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
    Commented Feb 26, 2018 at 22:21
  • This code working perfectly could you please share your issue in more details.
    – Pratik
    Commented Feb 27, 2018 at 5:35
4

I come from the future with good news: this is now possible totally out of box for Magento 2.3.3 and above.

Stores > Configuration > Advanced > Developer > JS Settings > Move JS code to the bottom of the page

move js code

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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