How to use the `PerformanceNavigationTiming` API to get page load time? - javascript

I am trying to to use the PerformanceNavigationTiming API to generate a page load metric.
The MDN API document linked above says that the PerformanceEntry.duration should give me what I need because it:
[r]eturns a timestamp that is the difference between the PerformanceNavigationTiming.loadEventEnd and PerformanceEntry.startTime properties.
However, when I check this property, I get simply 0. I'm accessing this API from within a React hook that runs a useEffect function that wait for the window load event and then checks the api like so:
export const useReportPageLoadTime = () => {
useEffect(() => {
const reportTime = () => {
let navPerformance: PerformanceEntry
navPerformance = window.performance.getEntriesByType('navigation')[0]
console.log({
duration: navPerformance.duration,
blob: navPerformance.toJSON()
})
}
if (document.readyState === 'complete') {
reportTime()
return null
} else {
window.addEventListener('load', reportTime)
return () => window.removeEventListener('load', reportTime)
}
}, [])
}
As you can see there, I also call toJSON on the performance entry and indeed it shows that the values upon which duration (startTime and loadEventEnd) are both 0 as well:
Does anyone know why I am getting this value?

I was finally able to get this to work using a different method than the event listener. It certainly is logical that the data should be ready when the load event fires, but the only way I was able to get the data was to use another feature of the Performance API: the PerformanceObserver, which fires a callback when a new piece of data has become available.
Here is the code that worked for me:
export const useReportPageLoadMetrics = () => {
useEffect(() => {
const perfObserver = new PerformanceObserver((observedEntries) => {
const entry: PerformanceEntry =
observedEntries.getEntriesByType('navigation')[0]
console.log('pageload time: ', entry.duration)
})
perfObserver.observe({
type: 'navigation',
buffered: true
})
}, [])
}

Related

is there any way to find out which endpoints are triggered on a page?

I have a generic implementation to fire a page_view google analytics event in my react application every time there's a route change:
const usePageViewTracking = () => {
const { pathname, search, hash } = useLocation();
const pathnameWithTrailingSlash = addTrailingSlashToPathname(pathname) + search + hash;
useEffect(() => {
invokeGAPageView(pathnameWithTrailingSlash);
}, [pathname]);
};
export default usePageViewTracking;
This works fine, but I need to fire ga4 page_view events with custom dimensions and if the page doesn't have some data, I should not send it in page_view event.
I turned my previous code into this:
const usePageViewTracking = () => {
const { pathname, search, hash } = useLocation();
const subscriptionsData = useAppSelector(
(state) => state?.[REDUX_API.KEY]?.[REDUX_API.SUBSCRIPTIONS]?.successPayload?.data
);
useEffect(() => {
sendPageViewEvent({ subscriptionsData });
}, [pathname, subscriptionsData]);
};
export default usePageViewTracking;
sendPageViewEvent is where I collect most of the information I need to be sent, and currently is like this:
export const sendPageViewEvent = ({ subscriptionsData }: SendPageViewEventProps): void => {
const { locale, ga } = window.appData;
const { subscriptions, merchants, providers } =
prepareSubscriptionsData({ subscriptionsData }) || {};
const events = {
page_lang: locale || DEFAULT_LOCALE,
experiment: ga.experiment,
consent_status: Cookies.get(COOKIES.COOKIE_CONSENT) || 'ignore',
...(subscriptionsData && {
ucp_subscriptions: subscriptions,
ucp_payment_providers: providers,
ucp_merchants: merchants,
}),
};
sendGA4Event({ eventType: GA4_EVENT_TYPE.PAGE_VIEW, ...events });
};
So as you can see, I have some dimensions that are always sent, and some that are conditionally sent (subscriptionsData).
The problem
The problem with this implementation is that once the page renders, it waits for subscriptionData to be available to fire the event, which would be ok, if this data would be fetched in all pages. If a page doesn't have this data, I still need to send the event, just not attach subscriptions dimensions into it.
I tried different approaches in my application, like:
going to each page and firing it individually, but since it's a huge application, it would require a huge refactoring that turns out to not to be justified for analytics purposes.❌
Having some sort of config file to define which routes fire which endpoints, but this is a terrible and unmaintainable idea ❌
Now if there would be a way to know based on the redux store how figure out which endpoints are being triggered on a page, maybe I could then detect it and decide if I should wait for this property to be available or fire the event without it.
PS: there will be more fetched data from different endpoints that I'll have to fire on page_view experiment too... and it's an SPA aplication (so everything is using CSR).
Any ideas are welcome! :D

Nuxt handle redirect after deletion without errors : beforeUpdate direction not working?

So I have this nuxt page /pages/:id.
In there, I do load the page content with:
content: function(){
return this.$store.state.pages.find(p => p.id === this.$route.params.id)
},
subcontent: function() {
return this.content.subcontent;
}
But I also have an action in this page to delete it. When the user clicks this button, I need to:
call the server and update the state with the result
redirect to the index: /pages
// 1
const serverCall = async () => {
const remainingPages = await mutateApi({
name: 'deletePage',
params: {id}
});
this.$store.dispatch('applications/updateState', remainingPages)
}
// 2
const redirect = () => {
this.$router.push({
path: '/pages'
});
}
Those two actions happen concurrently and I can't orchestrate those correctly:
I get an error TypeError: Cannot read property 'subcontent' of undefined, which means that the page properties are recalculated before the redirect actually happens.
I tried:
await server call then redirect
set a beforeUpdate() in the component hooks to handle redirect if this.content is empty.
delay of 0ms the server call and redirecting first
subcontent: function() {
if (!this.content.subcontent) return redirect();
return this.content.subcontent;
}
None of those worked. In all cases the current page components are recalculated first.
What worked is:
redirect();
setTimeout(() => {
serverCall();
}, 1000);
But it is obviously ugly.
Can anyone help on this?
As you hinted, using a timeout is not a good practice since you don't know how long it will take for the page to be destroyed, and thus you don't know which event will be executed first by the javascript event loop.
A good practice would be to dynamically register a 'destroyed' hook to your page, like so:
methods: {
deletePage() {
this.$once('hook:destroyed', serverCall)
redirect()
},
},
Note: you can also use the 'beforeDestroy' hook and it should work equally fine.
This is the sequence of events occurring:
serverCall() dispatches an update, modifying $store.state.pages.
content (which depends on $store.state.pages) recomputes, but $route.params.id is equal to the ID of the page just deleted, so Array.prototype.find() returns undefined.
subcontent (which depends on content) recomputes, and dereferences the undefined.
One solution is to check for the undefined before dereferencing:
export default {
computed: {
content() {...},
subcontent() {
return this.content?.subcontent
👆
// OR
return this.content && this.content.subcontent
}
}
}
demo

How to correctly add event listener to React useEffect hook?

I am trying to add an event listener to an Autodesk Forge viewer. This is an application built on React and this is the code I am trying:
const selectEvent = () => {
let viewer = window.NOP_VIEWER;
viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
};
This runs perfectly when called from a button onClick:
<Button onClick={() => selectEvent()}>Add</Button>
However, I would like the event listener to turn on when the page is loaded, so I tried useEffect:
useEffect(() => {
let viewer = window.NOP_VIEWER;
if (viewer) {
selectEvent();
}
}, []);
Even after trying some modifications, I could not get it to work. Nothing happens, so I suspect the event listener never gets added. Looking around at other solutions, event listeners are usually loaded with useEffect, but I am not sure what I am doing wrong. Any tips would be appreciated!
edit: It does enter the if statement, as a console.log works
Some background (might be relevant):
The viewer is loaded from a useEffect
useEffect(() => {
initializeViewer(props);
}, []);
and the viewer can be accessed as shown in the code above.
Try some thing like this.
When ever change in viewer and viewer is available, then you register the event.
Deregister the event handler as return function to hook
useEffect(() => {
if (viewer) {
viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
}
return () => { /* do the removeEventLister */ }
}, [viewer]);
Try this
NOP_VIEWER is a global variable to access the current Viewer
you need to remove the event listener after listening otherwise it will cause memory leak
useEffect(()=>{
NOP_VIEWER.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
return()=>{NOP_VIEWER.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
}))}
},[])
or if it doesn't work
useEffect(()=>{
let viewer= window.NOP_VIEWER
viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
},[])

NodeJS events triggering multiple times in electron-react app

I have a package (Let's say PACKAGE_A) written to do some tasks. Then it is required by PACKAGE_B. PACKAGE_A is a node script for some automation work. It has this Notifier module to create and export an EventEmitter. (The Whole project is a Monorepo)
const EventEmitter = require('events');
let myNotifier = new EventEmitter();
module.exports = myNotifier;
So in some functions in PACKAGE_A it emits event by requiring myNotifier, and also in the index.js of PACKAGE_A, I export functions (API exposed to the other packages) and the myNotifier by requiring it again.
const myNotifier = require('./myNotifier);
const func1 = () => {
// some function
return something;
}
module.exports = {func1, myNotifier}
Then I import the PACKAGE_A in PACKAGE_B and use the API functions exposed with the notifier. PACKAGE_B is an electron app with a React UI.
Below is how the program works.
I have a console output window in the electron app (React UI, UI_A). <= (keep this in mind)
When I click a button in UI_A it fires a redux action (button_action). Inside the action, a notification is sent to an event which is listened in the electron code using ipcRenderer.
ipcRenderer.send('button-clicked', data); // <= this is not the full code of the action. It's bellow.
Then in the electron code (index.js), I require another file (UI_A_COM.js which houses the code related to UI_A in electron side). The reason is code separation. Here's part of the code in index.js related to the electron.
const ui_a_com = require('./electron/UI_A_COM');
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
resizable: false,
});
mainWindow.loadURL('http://localhost:3000');
const mainMenu = Menu.buildFromTemplate(menuTemplate);
ui_a_com (mainWindow);
};
Alright. Then in UI_A_COM.js, I listen to that triggered event button-clicked.
ipcMain.on('button-clicked', someFunction);
which runs the code from PACKAGE_A and return a result. So now when PACKAGE_A runs, it emits some events using myNotifier. I listen to them in the same file (UI_A_COM.js), and when those events are captured, I again send some events to React UI, which is subscribed when button_action fired.
myNotifier.on('pac_a_event_a', msg => {
mainWindow.webContents.send('ui_event_a', msg); // code in `UI_A_COM.js`
});
Here's the full code for the action. (Did not provide earlier because you'll get confused)
export const buttonAction = runs => {
return dispatch => {
ipcRenderer.send('button-clicked', data);
ipcRenderer.on('ui_event_a', (event, msg) => {
dispatch({ type: SOME_TYPE, payload: { type: msg } });
});
};
};
This will show the msg in the UI_A console.
So this is the task I'm doing. The problem is when I click the button; it works perfectly for the first time. But when I click the button on the second time, it received two messages. Then when I click the button again, three messages and it keeps growing. (but the functions in the PACKAGE_A only executes one time per button press).
Let's say the message from PACKAGE_A emitted is 'Hello there' per execution.
When I press the button 1st time a perfect result => Hello there, When I click the button again => Hello there Hello there, When I click it again => Hello there Hello there Hello there.
It's kept so on. I think my implementation of EventEmitter has some flows. So why it's happening like this? Is it EventEmitter or something else? What am I doing wrong here?
By default the electron-react boilerplate doesnt define the ipcRenderer.removeAllListeners method. So you have to first go to the main/preloads.ts file and add them :
removeListener(channel: string, func: (...args: unknown[]) => void) {
ipcRenderer.removeListener(channel, (_event, ...args) => func(...args));
},
removeAllListeners(channel: string) {
ipcRenderer.removeAllListeners(channel);
},
Then go to the renderer/preload.t.s declaration file and add the declarations too:
removeListener(
channel: string,
func: (...args: unknown[]) => void
): void;
removeAllListeners(channel: string): void;
After that make sure to clean all listeners in the cleanup function of your useEffects each time you listen to an event fired. This will prevent multiple firing.
useEffect(() => {
window.electron.ipcRenderer.on('myChannel', (event, arg) => {
// do stuffs
});
return () => {
window.electron.ipcRenderer.removeAllListeners('myChannel');
};
});
I think you should return a function that call ipcRenderer.removeAllListeners() in your component's useEffect().
Because every time you click your custom button, the ipcRenderer.on(channel, listener) is called, so you set a listener to that channel agin and agin...
Example:
useEffect(() => {
electron.ipcRenderer.on('myChannel', (event, arg) => {
dispatch({ type: arg });
});
return () => {
electron.ipcRenderer.removeAllListeners('myChannel');
};
});

ReactJS server-side rendering, setTimeout issue

On my ReactJS App I used setTimeout to defer some Redux action:
export const doLockSide = (lockObject) => (dispatch) => {
const timerId = setTimeout(() => {
dispatch({
type: CONSTANTS.TOPICS_SET_CURRENT_TOPIC_LOCKED_SIDE,
payload: { id: lockObject.topicId, side: lockObject.side, locked: false }
});
}, lockObject.unlockTimeout);
dispatch({
type: CONSTANTS.TOPICS_SET_CURRENT_TOPIC_LOCKED_SIDE,
payload: { id: lockObject.topicId, side: lockObject.side, timerId, locked: true }
});
};
The lockObject comes from the server, so this code is a part of async Redux actions chain. It worked fine, but when I tried to make this functionality to be a part of server side rendering process, it broke the App. I understand the difference between Browser and NodeJS runtime environments and the difference between its implementations of setTimeout. Specifically my timerId could not be processed by Node due to it's an object, while my Redux reducer treats it as an integer. But the main problem is that during server side rendering Node fires setTimeout callback on the server side...
The question. I have some redux-based proccess that should be deferred in some cases including the App start. How can I do it satisfying the requirement of server-side rendering?
After some research I was able to apply the following approach.
1) Push the deferred action data into some special storage in case of server-side rendering, and run it "as is" in case of Browser:
import { _postRender } from '../utils/misc';
const doLockSideUI = (dispatch, lockObject) => {
// the body of previous version of doLockSide inner function
const timerId = setTimeout(() => {/*...*/}, lockObject.unlockTimeout);
dispatch(/*...*/);
};
export const doLockSide = (lockObject) => (dispatch) => {
if(typeof window === 'undefined') { // server-side rendering case
_postRender.actions.push({
name: 'doLockSide',
params: lockObject
});
}
else { // Browser case
doLockSideUI(dispatch, lockObject);
}
};
Where utils/misc.js has the following entity:
// to run actions on the Client after server-side rendering
export const _postRender = { actions: [] };
2) On the server I've imported that _postRender object form utils/misc.js and pushed it to render parameters when all redux-store data dependencies had been resolved:
const markup = renderToString(/*...*/);
const finalState = store.getState();
const params = { markup, finalState, postRender: { ..._postRender } };
_postRender.actions = []; // need to reset post-render actions
return res.status(status).render('index', params);
_postRender.actions has to be cleaned up, otherwise _postRender.actions.push from p.1 will populate it again and again each time the Client had been reloaded.
3) Then I provided my post-render actions the same way as it is done for preloaded state. In my case it is index.ejs template:
<div id="main"><%- markup %></div>
<script>
var __PRELOADED_STATE__ = <%- JSON.stringify(finalState) %>;
var __POST_RENDER__ = <%- JSON.stringify(postRender) %>;
</script>
4) Now I need to call my __POST_RENDER__ actions with given params. For this purpose I updated my root component's did-mount hook and dispatch an additional action which handles the post-render action list:
componentDidMount() {
console.log('The App has been run successfully');
if(window.__POST_RENDER__ && window.__POST_RENDER__.actions.length) {
this.props.dispatch(runAfterRender(window.__POST_RENDER__.actions));
}
}
Where runAfterRender is a new action that is being imported from ../actions/render:
import { doLockSide } from './topic'
export const runAfterRender = (list) => (dispatch) => {
list.forEach(action => {
if(action.name === 'doLockSide') {
dispatch(doLockSide(action.params));
}
// other actions?
});
};
As you can see, it's just a draft and I was forced to import doLockSide action from p.1 and call it explicitly. I guess there may be a list of possible actions that could be called on the Client after server-side rendering, but this approach already works. I wonder if there is a better way...

Categories