managing javascript memory usage in dynamically loaded pages - javascript

At the risk of being down-voted for bring to vague or asking an opinion I am unsure how to handle this situation properly, or if it even matters.
I am developing a single page web application where all pages except the main page (with the menu system) are loaded dynamically via ajax. All of the javascript libraries I use (jquery, bootstrap, kendo, and others) are loaded in the main page as well as my application.js and css files.
Each page I load requires it's own javascript and has a corresponding js files named the same as the html file (for my own sanity). When the page is downloaded and set in the content element, the page js runs, which is great.
I soon realized that the page js is adding any variables it creates to the global javascript space. Ideally upon the loading of a different page those variables would go away so javascript garbage collection could eventually reclaim that memory.
To facilitate this I started to re-use the same names for common structures, like kendo viewmodels so when a new page is loaded it replaces any global variables of the same name.
In some cases where there are specialized variables I set their values to null to release the memory in the global ajax loader function.
I'm considering adding a release() function to all pages that does this and call it before the page is unloaded. Since the release function is global and each page redefines it calling it prior to swapping out the page should work.
My questions:
does it matter? should I care about a few Kb of ram that may be left lying around..
Are there better ways of handling this?
Am what I'm now doing even effective? Does javascript garbage collection clean up un-referenced ram like I'm thinking it does?
Followup after Giacomo's answer:
Giacomo your technique works in the sense that the page functions normally and the kendo bindings have access to local viewmodel variable created in the Page routine. My concern is that I don't know how it is working. I omitted the var page = Page(); assignment and simply include Page(); at the end of the script (outside of the Page() function) and there seem to be no detrimental effects.
The way I see it is the script is run when the page is set into it's parent element's html (a div), this defines then runs the Page() function, which creates all the local variables and kendo widgets/bindings on the page, then it falls out of scope. Once the Page() function has completed, it ends and there should be nothing left, however the viewmodel I created does remain because the page and it's binding all work, click events defined in Page() are executed, and so on. So, when will all that stuff go away? Will it be de-allocated when I replace the parent's html with a new page? I don't see how...of course whatever kendo is doing to keep a reference to the viewmodel seems to keep it alive, I am not confident that it gets released when the page disappears, which is my whole intent.
It could be that the page = new Page(); is essential to the de-allocation process...but I don't know how to determine that.
Possible Solution?
Can someone comment on the worth of this:
//enters here on ajax page load after inserted into DOM
var pageData = {
someData: 999,
someArray: ["one","two"],
functionOne: function(arg) {
//stuff
},
functionTwo: function(arg) {
//stuff
}
};
//do some work
pageData.functionOne(pageData.someData);
pageData.functionTwo("abc");
//called before page is removed from DOM
function releasePage() {
pageData = null;
}
//GC eventually cleans it up

you can put the js code of each page in a closure, like:
function Page() {
var var1;
var var2;
function myFunc() {
//...
}
}
and then assign it to a variable upon the "page switching"
var page = new Page();
you can keep reassigning the same variable every time

Related

How to use javascript without appending it to DOM

I am developing an Single Page Application (SPA) from scratch. I am doing it from scratch using only HTML, CSS and vanilla JavaScript and not using any external frameworks.
My application will initially load Web page but upon navigating to some other page say page2, it will only load required data and functions about other page2 from page2.js and not reload the entire Web page.
To use the JavaScript I will append it to body. But the problem is that when I navigate same page again it will append the same JavaScript again. The more pages I visit the more scripts are attached.
I have tried removing existing script tag in favour or upcoming script and it works good, but is there a way that I don't have to append script to DOM in the first place?
So my question is, is there a way we can parse (not just plain read) or execute JavaScript file without using any physical medium (DOM)
Although I am expecting pure JavaScript, libraries would also work, just need a logical explaination
So my question is, is there a way we can parse (not just plain read) or execute JavaScript file without using any physical medium (DOM)
Yes, you can. How you do it depends on how cutting-edge the environment you're going to support is (either natively, or via tools that can emulate some things in older environments).
In a modern environment...
...you could solve this with dynamic import, which is new in ES2020 (but already supported by up-to-date browsers, and emulated by tools like Webpack and Rollup.js). With dynamic import, you'd do something like this:
async function loadPage(moduleUrl) {
const mod = await import(moduleUrl);
mod.main();
}
No matter how many times it's requested, within a realm a module is only loaded once. (Your SPA will be within a realm, so that works.) So the code above will dynamically load the module's code the first time, but just give you back a reference to the already-loaded module the second, third, etc. times. main would be a function you export from the module that tells it you've come (back) to the "page". Your modules might look like this:
// ...code here that only runs once...
// ...perhaps it loads the markup via ajax...
export function main() {
// ...this function gets called very time the user go (back) to our "page"
}
Live example on CodeSandbox.
In older environments...
...two answers for you:
You could use eval...
You can read your code from your server as text using ajax, then evaluate it with eval. You will hear that "eval is evil" and that's not a bad high-level understanding for it. :-) The arguments against it are:
It requires parsing code; some people claim firing up a code parser is "slow" (for some definition of "slow).
It parses and evaluates arbitrary code from strings.
You can see why #2 in particular could be problematic: You have to trust the string you're evaluating. So never use eval on user-supplied content, for instance, in another user's session (User A could be trying to do something malicious with code you run in User B's session).
But in your case, you want and need both of those things, and you trust the source of the string (your server), so it's fine.
But you probably don't need to
I don't think you need that, though, even in older environments. Your code already knows what JavaScript file it needs to load for "page" X, right? So just see whether that code has already been loaded and don't load it again if it is. For instance:
function loadPage(scriptUrl, markupUrl) {
// ...
if (!document.querySelector(`script[src="${scriptUrl}"]`)) {
// ...not found, add a `script` tag for it...
} else {
// ...perhaps call a well-known function to run code that should run
// when you return to the "page"
}
// ...
}
Or if you don't want to use the DOM for it, have an object or Map or Set that you use to keep track of what you've already loaded.
Go back to old-school -- web 1.0, DOM level 1.0, has your back. Something like this would do the trick:
<html><head>
<script>
if (!document.getElementById('myScriptId')) {
document.write('<script id="myScriptId" src="/path/to/myscript"></scri' + 'pt>');
}
</script>
This technique gets everybody upset, but it works great to avoid the problems associated with doing dynamic loading via DOM script tag injection. The key is that this causes the document parser to block until the script has loaded, so you don't need to worry about onload/onready events, etc, etc.
One caveat, pull this trick near the start of your document, because you're going to cause the engine to do a partial DOM reparse and mess up speculative loading.

Global variable created inside CrossRider's appAPI.ready() function not available to page?

I am trying to create a Chrome extension using CrossRider and am struggling with how to create a global variable.
My extension is essentially going to dynamically append a few JavaScript files to the page if a button is clicked, and I need it to also create a global variable and set some properties.
I tried the following:
appAPI.ready(function($) {
console.log('inside crossrider extension ready()');
window.foobar = 'barfoo';
return;
});
When I refresh the page, the message inside crossrider extension ready() is printed out to the console, so I know the extension is loaded and working, but when I try executing window.foobar or foobar in the console an error is thrown saying it's undefined.
This is my first time creating an extension, so what am I missing here? Why isn't the global variable I create inside of CrossRider's appAPI.ready() function available outside of it?
I can't find a duplicate target, so I'll explain what's happening.
I don't know the Crossrider terminology, but when a Chrome extension executes code in a page, this is called a Content Script.
However, the code does not execute in the same context as the page itself. It's called an isolated world and means, among other things, that the window object is not shared.
Content scripts execute in a special environment called an isolated world. They have access to the DOM of the page they are injected into, but not to any JavaScript variables or functions created by the page. It looks to each content script as if there is no other JavaScript executing on the page it is running on. The same is true in reverse: JavaScript running on the page cannot call any functions or access any variables defined by content scripts.
So if you want to set a variable accessible to the page, you need to set it in the page context. How? There are many ways, but they all amount to inserting a <script> element into the page.
var script = document.createElement('script');
script.textContent = "window.foobar = 'barfoo';";
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
All of that assumes that you don't control the page in question. If you do, there are other ways of communicating the the page, because DOM is shared. You can, for instance, raise a custom DOM event if the page listens to it.

setInterval breaks on window.locaion / window.open

I'm doing some code that requires actions on multiple sites (get some data switch to another site etc.) in a loop.
I'm trying to do this using setInterval().
Simplified, the task looks like this when launched in a console:
function checkit() {
window.location='http://www.google.pl';
}
var nre = setInterval(checkit,5000);
I have tried launching this script (in more complicated forms through different measures, from bookmarklet, from server side script etc, the interval runs OK in my original code, even does everything what I require in a loop until another page is called (through window.open or window.location). than the loop just seizes to execute.
I'm pretty new to JS (2 days experience) so I'm probbably doing something uterly stupid. Any Advice on how to get this thing going (is this even possible)?
Best regards
The problem you're going to have is that JS doesn't stay from page to page, so once the page changes, that loop goes away. You'll need to have the JS on each page you're wanting to visit to continue flow and even then, the variables are nuked when you change pages.
The only way to circumvent this issue is by storing a serialized object (or JSON string) within the window.name value which is remember across pages and domains within that tab.

Avoid duplicate Javascript inside iframes

Here is a wireframe to ilustrate the structure of a legacy project that shows some performance issue:
For all dialogs (From jQuery UI) open a new iframe are created and all js from Home are re-downloaded and all objects are re-instanced. Can I create a reference from jQuery from Home to all new iframes and work in each iframe isolated scope?
For example:
[Home scope]
$("#some-el").data('foo', 'bar');
console.log($("#some-el").data('foo')); // results bar
[App1 scope]
//after defined in Home first run
console.log($("#some-el").data('foo')); // results undefined
PS: Remember this is a legacy architeture and all solutions must be consider this scenario.
I've encountered this situation before. One approach is to define some javascript that gets loaded into the iframes that just reroutes any function calls to top.functionCall() instead of containing their actual definition. It becomes very simple if all of your functions are under one namespace, like so:
Parent window js:
var namespace = (function () {
// all of your functions are in here as properties of namespace
})();
iframe window js:
var namespace = top.namespace;
One issue with this is any context sensitive functions (functions that rely or operate on the window object) will most likely break.
Actually, if all of these are hosted in the same place, the browser will NOT be downloading the files multiple times. Rather, it will be caching the first result, and then pulling from cache in the second. Iframes are treated as a separate context, so you won't need to worry about variable or form conflicts.
Assuming that downloading the same file twice is your primary concern, then you should be ok there.
An alternative design would be to use AJAX instead of iframed content - but having been where you are in working with legacy apps, I realize how hard that can be to do without real JSON / REST calls available. One thing I've done is changed the views inside the iframes to be "partials," returning only the necessary HTML contents without the HTML head etc., and loading them using $.load(). This gets complex as you will need to execute bindings post-load and carefully track form ID's etc., but it can be done.

Lazy loading in Knockout JS

I'm trying to load and parse json data from an external source into a table via Knockout JS. So far, everything has been successful through the following code:
// Snippet
var self = this;
self.notices = ko.observableArray([]);
self.currentTab = ko.observable(5);
ko.computed(function() {
$.getJSON('http://json.source.here.com/tab/'+ko.toJS(self.currentTab), function(threads) {
if (threads !== null) {
self.notices(threads);
} else {
self.notices([]);
}
});
}, self.notices);
When a user clicks on a certain tab it would load the json data (forum threads) based on the selected tab value (self.currentTab) onto the table in the form of rows (self.notices).
Everything works as expected however, I noticed while browsing around other pages that do not have the above bindings, the json is still being loaded ($.getJSON is fired). I'm concerned that this may have some detrimental effects on the performance of my website as it is loading the json source even though it is not needed.
EDIT: I figured this out through Google Chrome's developer console.
I currently have my view model in a JavaScript file that is being used by every other pages as well. It consists of bindings for all the pages.
My question is, how can I load the json data on a specific page or only when the bindings are present - lazy loading? Preferably, I would like to keep all the bindings in a single JavaScript file, I do not want to separate them out and load them on a per page basis.
Here is an article that I wrote on a simliar topic a little while back: http://www.knockmeout.net/2011/06/lazy-loading-observable-in-knockoutjs.html
In your case, I think that you really want to add some guards around the $.getJSON call to ensure that it is only making AJAX requests when you are in the appropriate state (on the appropriate tab).
Along with that, the blog post describes using the deferEvaluation flag on a computed observable to ensure that the logic does not run until someone binds against the computed observable (in your case, you have an anonymous computed observable, but you could add it to your view model as a property and bind against it in your view. Without this flag, the evaluation code will run when you create the computed observable, which is not desirable in your case.

Categories