Is there an easy way in python to find the bpy.data.images that are used in a node?
Recurse the tree.
In question code, the major rate determining step will be checking the list for dupe to append, often quicker to add them all and use a set if only unique members are of interest.
Suggest can use list comprehension, or recursively walk the tree.
Here is an example, pass a node tree to a method and walk its nodes, if it has an image property yield it, if it has a node tree yield from it.
Personally I like the simplicity of yield syntax usage for traversing a tree, and the low memory hogging nature of generators, however they need a little tweaking to optimize for speed, see below
import bpy

def images_in_tree(node_tree):
    for node in node_tree.nodes:
        if hasattr(node, &quot;image&quot;):
            yield node.image
        if hasattr(node, &quot;node_tree&quot;):
            yield from images_in_tree(node.node_tree)
        
for mat in bpy.data.materials:
    if mat.use_nodes:
        print(set(filter(None, images_in_tree(mat.node_tree))))

Note could yield above getattr(node, &quot;image&quot;, None) to avoid testing and filter out the Nones, however it may be a better idea to yield the node_tree, and the node.  The ID data of the node_tree node_tree.id_data will be a material or a node group.
Just the number of unique images in tree.
In answer to

Keep track of how many images are in use in a Material (Or node_tree)
in real time, printed on the UI, without having to press buttons or
the like

Instead of yielding the images returns only a count.
Used a boolean default dictionary to tag items. Any item is given the default value false
In response to

fyi, you can't use tag here. Or any optimization like caching.

Have tagged both the nodegroups and images with one dict to  make the method directly usable in a draw method, instead of using say a getter of a material property,
bpy.types.Material.image_users = IntProperty(get=num_images)

In which case could lay it out with
layout.prop(mat, &quot;image_users&quot;)

and using the collection.tag(False) would be Ok in this context, and removes the need for the default dictionary.
As noted yielding and recursion can be slow in python, however using functools.lru_cache seriously fixes this see similar gain here https://blender.stackexchange.com/a/199075/15543 as well as other optimizations used. See lru_cache link re expiring or clearing cache on some interval
Basically it is caching the result of a call to memory.
import bpy
from collections import defaultdict
from functools import lru_cache

@lru_cache(16) # play with this
def num_images(mat):
    tag = defaultdict(bool)
    
    def unique(node):
        img = getattr(node, &quot;image&quot;, None) 
        if tag[img]:
            return False
        tag[img] = True
        return img is not None

     
    def images_in_tree(node_trees):
        for node_tree in node_trees:
            if tag[node_tree]:
                continue
            tag[node_tree] = True
            yield sum(unique(n) for n in node_tree.nodes)
            yield from images_in_tree((n.node_tree  for n in node_tree.nodes if hasattr(n, &quot;node_tree&quot;)))
    return sum(images_in_tree((mat.node_tree,)))

# make an int material property 
bpy.types.Material.image_users = bpy.props.IntProperty(get=num_images.__wrapped__)
# tack a draw method to text editor footer to test
from random import random
def draw(self, context):
    mat = context.object.active_material
    if mat:
        self.layout.prop(mat, &quot;image_users&quot;)
        if random() &lt;= 0.5:
            num_images.cache_clear()
        self.layout.label(text=f&quot;{num_images(mat)}&quot;)
        
bpy.types.TEXT_HT_footer.prepend(draw)

The material property uses an uncached version of the method above, num_images.__wrapped__ the label uses the cached method.  The cache is cleared each time a random is below a threshold. The expectation above is that it is only calculated approximately every 2nd time.
Other methods to consider would be using a counter on the UI class, clearing in a draw, poll or __init__ method of classes in different regions, a draw call back.. (Or as mentioned prior expire the cache) or simply not use it at all.

Extra image tacked into test file, showing 8 images in material, removed shows 7, undo back to 8