Shared Web Worker with Dedicated Web Worker as failover - Lazy Load Example

jkon 2 Tallied Votes 459 Views Share

I wanted to post something in Daniweb for a while but I didn't had anything new. Recently I decided to write a new implementation for websocket connections client side using Shared Web Workers. Shared Web Workers are supported today by most browsers for desktop but few for mobile devices , so it had to have a fallover mechanism to Dedicated Web Workers. I searched but I didn't found an example , so I am posting it here. Moreover since we will have a worker not in main thread it would be great to have the option for callback functions for other uses than WebSockets. I implemented these without any framework to keep it as simple as possible and here it is an example of this for something common and useful , lazy loading images . It needs to be modified if you are going to use it to be adjusted to your needs and to add error handling and arguments checking at least.

We are going to user five files in a folder. Two images: one imageExample.jpg that is going to be the image to lazy load and a transparent1x1.png that is a transparent image 1px x 1px that we will use as placeholder src for the image before is loaded.

Lets start with the index.php (I am using PHP here but you can use any language)

<?php
/*
getting the root URL of the project is beyond the scope of this ,
but here is a quick implementation  
 */
$v = $_SERVER["SERVER_PROTOCOL"] ; 
$v = mb_strtolower(mb_substr($v, 0 , mb_strrpos($v, "/")));
if(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on")
{
    $v .= "s";
}
$root = $v."://".$_SERVER['HTTP_HOST']; 
$v = $_SERVER["PHP_SELF"];
$v = mb_substr($v, 0 , mb_strrpos($v, "/"));
$root .= $v;
?>
<html>
<head>
    <title>
        JavaScript Shared Web Worker with Dedicated Web Worker as failover 
        - Lazy Loading Images example
    </title>
    <style>
        /* IntersectionObserver works better with blocking elements to determine if are 
            intersected with the viewport */
        picture,img
        {
            display: inline-block;
            vertical-align: middle;
        }
    </style>
</head>
<body>
    <!-- all the img tags inside picture elements will be loaded with the 
        src of the noscript img tag when they appear on screen   -->
    <picture>
        <noscript>
            <img src="<?=$root?>/imageExample.jpg" alt="example image" width="800" height="500" />
        </noscript>
        <img src="<?=$root?>/transparent1x1.png" alt="example image" width="800" height="500" />
    </picture>
    <script type="text/javascript">
        /*
            load the example.js after DOMContentLoaded or if that doesn't fired on window.onload 
            and then call the init of the example object that instantiated there 
        */
        var _fncl=false;
        _fnc=function()
        {
            _fncl=true;
            var s = document.createElement("script");
            s.async=true;
            s.src="<?=$root?>/example.js?"+<?=date("U")?>;
            s.addEventListener("load", function(e)
            {
                    _.init("<?=$root?>");
            }, false);
            document.body.appendChild(s);
        };
        window.addEventListener("DOMContentLoaded", _fnc);
        window.onload=function(){if(!_fncl){_fnc();}};
    </script>  
</body>
</html>

As you can see it is very simple , now lets move to the main JavaScript file that will be loaded the example.js

/*
    this is an example of how the concept works it doesn't have error handling 
    or checking arguments for their correct type or even existence , 
    in real life projects those should implemented 
*/
const _ = new (function() // _ object holds the core functionalities  
{
    var _this = this; 
    var worker = null;
    var shared = false;
    var privateRoot = null;
    var index = 0; 
    var works = {}; 

    this.init = function(root)
    {
        privateRoot = root; 
        shared = !!window.SharedWorker; // if Shared Web Workers are supported
        //use Dedicated Web Worker when developing new methods 
        //  in order to view the errors in console 
        //shared = false;
        setWorker();
        app.init(); // ready to init the app object
    }

    this.root = function()
    {
        return privateRoot; 
    };

    this.getWorkKey = function()
    {
        index++;
        return "w"+index; 
    };

    /*
        request a work from the worker with callback function 
        dataObj is the object that will be send to the worker 
            , it must contains at least an action that will be a function of the worker 
            and any other data we might use there 
        fnc is the the callback function example: function(responseDataObj,argObj){} 
        argObj contains all other data we might need along with the response data   
    */
    this.work = function(dataObj,fnc,argsObj)
    {
        let wKey =  _this.getWorkKey();
        dataObj.wKey = wKey;
        works[wKey] = [fnc,argsObj];
        worker.postMessage(dataObj);
    };

    function setWorker()
    {
        if(shared)// instantiate the worker 
        {
            let w = new SharedWorker(_this.root()+"/worker.js");
            worker = w.port;
            worker.start();
        }
        else 
        {
            worker = new Worker(_this.root()+"/worker.js");
        }

        var handler = function(event)//handle incoming events from worker
        { 
            let data = event.data;
            if("n" in data && data.n == "u") // pong (ping response) that this page is still alive 
            {
                worker.postMessage({n:"y"});
            }
            else if("log" in data) // let the worker send messages to window console 
            {
                window.console.log(data.log);
                //worker.postMessage({n:"y"});
            }
            else if(data.wKey in works)
            {
                let arr = works[data.wKey]; // the array that is store in works object 
                // that has as first element the function and second the arguements object 
                arr[0](data,arr[1]); // call the callback function
                works[data.wKey] = null; 
                delete works[data.wKey]; // try to remove it from memory when gc will pass
            }
        };

        if(shared) // attach the handler to the worker 
        {
            worker.onmessage = (event) => {
                handler(event);
            };
        }
        else
        {
            worker.addEventListener('message', event => 
            {
                handler(event);
            });
        }
    }


})();


const app = new (function() //app object has specific code for this app 
{
    var _this = this; 
    this.lazyLoadObjerver = null;

    this.init = function()
    {
        setIntersectionObserver();
    };


    function setIntersectionObserver()
    {
        if ("IntersectionObserver" in window)
        {
            _this.lazyLoadObjerver = new IntersectionObserver(_this.lazyLoader);
        }
        _this.lazyLoad();
    }



    this.lazyLoad = function()
    {
        const els = document.getElementsByTagName("picture");
        for (const el of els)
        {
            if(_this.lazyLoadObjerver == null) // if the browser doesn't support IntersectionObserver
            {
                const src = el.querySelector("noscript").innerHTML
                    .match(/.*src=["']([^"]*)['"].*/)[1];// get src of the image
                el.querySelector("img").setAttribute("src",src);
            }
            else 
            {
                _this.lazyLoadObjerver.observe(el);
            }
        }
    };

    this.lazyLoader = function(entries, observer)
    {
        entries.forEach(function(entry) 
        {
            const el = entry.target;
            if (entry.isIntersecting) 
            {
                if(el.getAttribute("class") != "lz")
                {


                    const src = el.querySelector("noscript").innerHTML
                        .match(/.*src=["']([^"]*)['"].*/)[1];// get src of the image
                    // data object for the worker request
                    const dataObj = {action:"loadBlob",url:src};
                    // arguements object for the callback function
                    const argObj = {el:el}; 
                    // the callback function;
                    const fnc = function(data,arg){
                        const img = arg.el.querySelector("img");
                        arg.el.setAttribute("class","lz");
                        img.setAttribute("src",URL.createObjectURL(data.blob));
                    };
                    _.work(dataObj,fnc,argObj);

                }
            }
        });
    };

})();

And finally the worker.js file , it worth mentioning that if you are going to use Shared Web Workers I couldn't find any other way then a separate file.

(I am adding it as code snippet because without code snippet it doesn't allow me to post)

I hope to enjoy it , I added some comments inside the code but feel free to ask anything related. Also any other ideas would be welcomed

/*
	this is an example of how the concept works it doesn't have error handling 
	or checking arguments for their correct type or even existence , 
	in real life projects those should implemented 
*/
var senders = {};
var index = 0;
var alives = {};
var aliveFunc = null;
var shared = self.constructor.name == "SharedWorkerGlobalScope"; 
/*
	In most cases we will need to know how many pages are connected to this worker and all 
	the other approaches ( e.g. page hide event ) failed in one way or another, so we ping 
	them to see that they are alive every x ms that are defined here. It uses the same 
	Dedicated Web Workers to have only one logic ( and  to be sure that the browser does 
	what is supposed to). If anyone has any other idea that works I would be happy to read it. 
*/
var checkAliveMs = 10000;

function getSenderKey()
{
	index++;
	return "s"+index;
}

var handler = function(event) // handler for the request messages
{
	
	const data = event.data; 
	if("n" in data && data.n == "y") // ping response ( keep that alive )
	{
		alives[data.sKey] = 1;
	}
	else if("action" in data && typeof actions[data.action] == "function")
	{
		actions[data.action](data);
	}
};

if(shared) // get messages and attach the handler to the worker
{
	onconnect = (e) => 
	{
		var sKey = getSenderKey();
		senders[sKey] = e.ports[0];
  		senders[sKey].addEventListener("message", (event) => 
		{
			event.data.sKey = sKey;
			handler(event);
  		});
  		senders[sKey].start(); 
		if(aliveFunc != null)
		{
			clearTimeout(aliveFunc);
		}
		aliveFunc = setTimeout(checkAlives,checkAliveMs);
	};
}
else 
{
	var sKey = getSenderKey();
	senders[sKey] = self;
	senders[sKey].addEventListener('message', async event => 
	{
		event.data.sKey = sKey;
		handler(event);
	});
	if(aliveFunc != null)
	{
		clearTimeout(aliveFunc);
	}
	aliveFunc = setTimeout(checkAlives,checkAliveMs);
}	

function checkAlives() // send a ping message
{
	alives = {};
	for(var sKey in senders)
	{
		send({sKey : sKey,n:"u"});
	}
	clearTimeout(aliveFunc);
	aliveFunc = setTimeout(removeDeads,checkAliveMs);
}
		
function removeDeads() // remove those that didn't responded to ping message  
{
	for(var sKey in senders)
	{
		if(!sKey in alives || typeof alives[sKey] === "undefined")
		{
			delete senders[sKey];
		}	
	}
		
	if(Object.keys(senders).length == 0)
	{
		close();
	}
	else 
	{
		clearTimeout(aliveFunc);
		aliveFunc = setTimeout(checkAlives,checkAliveMs);
	}
}


function close() // no active connections 
{
	
}

function log(msg)// log a message to all connections window.console
{
	for(var sKey in senders)
	{
		senders[sKey].postMessage({log:msg});
	}
}

async function send(o) // the sender function 
{
	if(o.sKey in senders)
	{
		senders[o.sKey].postMessage(o);
	}
}
		
/* 
action functions that can be called from main thread 
*/


const actions = new (function() //app object has specific code for this app 
{
	//we must always set sender key and work key to the response data object 
	function responseObj(data)
	{
		return {
			sKey : data.sKey,
			wKey : data.wKey
		};
	}	
	
	this.loadBlob = async function(data)
	{
		const o = responseObj(data);
		const response = await fetch(data.url);
		o.blob = await response.blob();
		send(o);
	}
	
})();
jkon 638 Posting Whiz in Training Featured Poster

Since this is not in any production project yet , I am considering that it is best for W3C validation and SEO to have the noscript tag inside a span instead of a picture e.g.:

<span class="lz">
    <noscript>
        <img src="<?=$root?>/imageExample.jpg" alt="example image" width="800" height="500" />
    </noscript>
    <img src="<?=$root?>/transparent1x1.png" alt="example image" width="800" height="500" />
</span>

also to add a noscript style in head:

    <noscript>
        <style>
            .lz>img
            {
                display: none;
            }
        </style>
    </noscript>

accordingly the lazyLoad and lazyLoader app functions of the example.js :

    this.lazyLoad = function()
    {
        const els = document.querySelectorAll("span.lz");
        for (const el of els)
        {
            if(_this.lazyLoadObjerver == null) // if the browser doesn't support IntersectionObserver
            {
                const src = el.querySelector("noscript").innerHTML
                    .match(/.*src=["']([^"]*)['"].*/)[1];// get src of the image
                el.querySelector("img").setAttribute("src",src);
            }
            else 
            {
                _this.lazyLoadObjerver.observe(el);
            }
        }
    };

    this.lazyLoader = function(entries, observer)
    {
        entries.forEach(function(entry) 
        {
            const el = entry.target;
            if (entry.isIntersecting) 
            {
                if(el.getAttribute("class") == "lz")
                {
                    const src = el.querySelector("noscript").innerHTML
                        .match(/.*src=["']([^"]*)['"].*/)[1];// get src of the image
                    // data object for the worker request
                    const dataObj = {action:"loadBlob",url:src};
                    // arguements object for the callback function
                    const argObj = {el:el}; 
                    // the callback function;
                    const fnc = function(data,arg){
                        const img = arg.el.querySelector("img");
                        arg.el.removeAttribute("class");
                        img.setAttribute("src",URL.createObjectURL(data.blob));
                    };
                    _.work(dataObj,fnc,argObj);

                }
            }
        });
    };

I am not sure it worth the trouble to have a span with a noscript tag instead of data-src directly in img tag , only for SEO reasons. I will check it in production code , there are also image sitemaps that solve the issue altogether. In my previous implementation of lazy loading I used data-src in the img tag and Google indexed the final src after the lazy loading , but I am not sure that this is the best practice.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.