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>
?
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();
}
}
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
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).